diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7cf3dea --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = .*/ +ignore = E203, E741, W503, W504 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..98c0c4d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [wkentaro] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 0000000..c12f453 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,45 @@ +name: Bug Report +description: Create a bug report +labels: 'bug' +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. + - type: markdown + attributes: + value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions ["Q&A / Help" section](https://github.com/wkentaro/labelme/discussions/categories/q-a-help). + - type: textarea + attributes: + label: Provide environment information + description: Please run `which python; python --version; python -m pip list | grep labelme` in the root directory of your project and paste the results. + validations: + required: true + - type: input + attributes: + label: What OS are you using? + description: 'Please specify the exact version. For example: macOS 12.4, Ubuntu 20.04.4' + validations: + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. + - type: markdown + attributes: + value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + - type: markdown + attributes: + value: Contributors should be able to follow the steps provided in order to reproduce the bug. + - type: markdown + attributes: + value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5f5744c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +contact_links: + - name: Ideas / Feature request + url: https://github.com/wkentaro/labelme/discussions/categories/ideas-feature-requests + about: Share ideas for new features + - name: Q&A / Help + url: https://github.com/wkentaro/labelme/discussions/categories/q-a-help + about: Ask the community for help + - name: Show and tell + url: https://github.com/wkentaro/labelme/discussions/categories/show-and-tell + about: Show off something you've made diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8df7415 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.9'] + PYTEST_QT_API: [pyqt5, pyside2] + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} + + # - name: Install system dependencies + # shell: bash -l {0} + # run: | + # if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + # sudo apt-get install -y coreutils + # sudo apt-get install -y xvfb herbstluftwm + # elif [ "${{ matrix.os }}" = "macos-latest" ]; then + # brew install coreutils + # brew install --cask xquartz + # fi + + - name: Set up Python + shell: bash -l {0} + run: | + conda install -q -y python=${{ matrix.python-version }} + which python + python --version + pip --version + + - name: Install dependencies + shell: bash -l {0} + run: | + if [ "${{ matrix.PYTEST_QT_API }}" = "pyside2" ]; then + conda install -q -y pyside2 -c conda-forge + # should be installed via pip + # else + # conda install -q -y pyqt=5 + fi + pip install -r requirements-dev.txt + + - name: Install main + shell: bash -l {0} + run: | + pip install . + + - name: Run ruff + shell: bash -l {0} + if: matrix.os != 'windows-latest' + run: | + ruff format --check + ruff check + + - name: Test with pytest + shell: bash -l {0} + if: matrix.os == 'ubuntu-latest' + env: + PYTEST_QT_API: ${{ matrix.PYTEST_QT_API }} + MPLBACKEND: 'agg' + run: | + sudo apt-get update + sudo apt-get install -y xvfb libqt5widgets5 + + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + export DISPLAY=:99 + + pytest tests + + - name: Run examples + shell: bash -l {0} + if: matrix.os != 'windows-latest' + env: + MPLBACKEND: agg + run: | + labelme --help + labelme --version + (cd examples/primitives && labelme_json_to_dataset primitives.json && rm -rf primitives_json) + (cd examples/tutorial && rm -rf apc2016_obj3_json && labelme_json_to_dataset apc2016_obj3.json && python load_label_png.py && git checkout -- .) + (cd examples/semantic_segmentation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) + (cd examples/instance_segmentation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) + (cd examples/video_annotation && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) + + pip install 'lxml<5.0.0' # for bbox_detection/labelme2voc.py + (cd examples/bbox_detection && rm -rf data_dataset_voc && ./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt && git checkout -- .) + + pip install cython && pip install pycocotools # for instance_segmentation/labelme2coco.py + (cd examples/instance_segmentation && rm -rf data_dataset_coco && ./labelme2coco.py data_annotated data_dataset_coco --labels labels.txt && git checkout -- .) + + - name: Run pyinstaller + shell: bash -l {0} + if: matrix.PYTEST_QT_API == 'pyqt5' + run: | + # Build the standalone executable + pip install pyinstaller + pyinstaller labelme.spec + dist/labelme --version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a5b9a29 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + if: startsWith(github.ref, 'refs/tags/') + + runs-on: ubuntu-latest + + steps: + - name: Build Changelog + id: github_release + uses: mikepenz/release-changelog-builder-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + configurationJson: | + { + "template": "#{{CHANGELOG}}\n\n
\nUncategorized\n\n#{{UNCATEGORIZED}}\n
\n\n- Tips and updates→ [X/Twitter <@labelmeai>](https://x.com/labelmeai)\n- Labelme Starter Bundle→ https://labelme.gumroad.com/l/starter-bundle", + "pr_template": "- #{{TITLE}} ##{{NUMBER}}", + "categories": [ + { + "title": "## 🚀 Features", + "labels": ["feature"] + }, + { + "title": "## ✨ Enhancement", + "labels": ["enhancement"] + }, + { + "title": "## 🐛 Fixes", + "labels": ["fix"] + }, + { + "title": "## 💬 Other", + "labels": ["other"] + } + ] + } + + - name: Create Release + id: create_release + uses: mikepenz/action-gh-release@v0.2.0-a03 + with: + body: ${{steps.github_release.outputs.changelog}} + + - name: Create release url file + run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt + + - name: Save release url file for publish + uses: actions/upload-artifact@v1 + with: + name: release_url + path: release_url.txt + + publish: + needs: [release] + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: '3.7' + + - name: Install main + shell: bash -l {0} + run: | + pip install . + + - name: Run pyinstaller + shell: bash -l {0} + run: | + pip install pyinstaller + pyinstaller labelme.spec + + - name: Load release url file from release job + uses: actions/download-artifact@v1 + with: + name: release_url + + - name: Get release file name & upload url + id: get_release_info + run: | + echo "::set-output name=upload_url::$(cat release_url/release_url.txt)" + + - name: Upload release executable on macOS & Linux + id: upload_release_executable_macos_linux + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.get_release_info.outputs.upload_url }} + asset_path: ./dist/labelme + asset_name: labelme-${{ runner.os }} + asset_content_type: application/octet-stream + if: runner.os != 'Windows' + + - name: Upload release executable on Windows + id: upload_release_executable_windows + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.get_release_info.outputs.upload_url }} + asset_path: ./dist/labelme.exe + asset_name: Labelme.exe + asset_content_type: application/octet-stream + if: runner.os == 'Windows' + + - name: Create dmg for macOS + run: | + npm install -g create-dmg + cd dist + create-dmg Labelme.app || test -f Labelme\ 0.0.0.dmg + mv Labelme\ 0.0.0.dmg Labelme.dmg + if: runner.os == 'macOS' + + - name: Upload release app on macOS + id: upload_release_app_macos + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.get_release_info.outputs.upload_url }} + asset_path: ./dist/Labelme.dmg + asset_name: Labelme.dmg + asset_content_type: application/octet-stream + if: runner.os == 'macOS' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2ddc71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.cache/ +/.pytest_cache/ + +/build/ +/dist/ +/*.egg-info/ + +*.py[cdo] + +.DS_Store +.idea/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..cd9cc35 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Wada" + given-names: "Kentaro" + orcid: "https://orcid.org/0000-0002-6347-5156" +title: "Labelme: Image Polygonal Annotation with Python" +doi: 10.5281/zenodo.5711226 +url: "https://github.com/wkentaro/labelme" +license: GPL-3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bde8540 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Copyright (C) 2016-2018 Kentaro Wada. +Copyright (C) 2011 Michael Pitidis, Hussein Abdulwahid. + +Labelme is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Labelme is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Labelme. If not, see . diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bb3ec5f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8764b2c --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +all: + @echo '## Make commands ##' + @echo + @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs + +lint: + ruff format --check + ruff check + +format: + ruff format + ruff check --fix + +test: + MPLBACKEND='agg' pytest tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ff9a77 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +

+ TrackMe +

+ +

+ Simple Tracking Annotation Tool based on LabelMe +

+ +## Description +This tool is built to integrate tracking visualization and annotation capabilities into LabelMe.
+Please feel free to use if your project/work includes visualization of multi-object tracking and editting of object information.
+TrackMe annotation format is compatible with LabelMe annotation format (.json) without conversion. + +
+TrackMe saves and displays the tracking information of multiple objects on the right. It generates unique colors for different combinations of object label and ID + +## Features +- Add/remove tracking ID. +- Associate boxes (assign IDs) for existing non-ID detection boxes in the video folder (SORT). +- Interpolate boxes in long video range in case no pre-defined detection boxes. +- Modify/Delete boxes of same info throughout a list of continuous frames. +- Display homegeneous color for same object info (for the sake of multi-view object tracking). + +## Installation +Please install Anaconda environment on your computer beforehand. + +conda create --name=trackGUI python=3.8
+conda activate trackGUI
+pip install -e .
+ +## Usage +conda activate trackGUI
+labelme + +## Note +All frames must have labeled boxes ++ Track from scratch: track from the first frame to the end frame with automatic ID assignment ++ Track from Current Frame w/ Annotation: track from the current (being opened) frame with the modified ID or manual ID assignment ++ Track from Current Frame w/0 Annotation: track from the current (being opened) frame with automatic ID assignment + diff --git a/examples/bbox_detection/.readme/annotation.jpg b/examples/bbox_detection/.readme/annotation.jpg new file mode 100644 index 0000000..d909620 Binary files /dev/null and b/examples/bbox_detection/.readme/annotation.jpg differ diff --git a/examples/bbox_detection/README.md b/examples/bbox_detection/README.md new file mode 100644 index 0000000..dbe65a3 --- /dev/null +++ b/examples/bbox_detection/README.md @@ -0,0 +1,25 @@ +# Bounding Box Detection Example + + +## Usage + +```bash +labelme data_annotated --labels labels.txt --nodata --autosave +``` + +![](.readme/annotation.jpg) + + +## Convert to VOC-format Dataset + +```bash +# It generates: +# - data_dataset_voc/JPEGImages +# - data_dataset_voc/Annotations +# - data_dataset_voc/AnnotationsVisualization +./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt +``` + + + +Fig1. JPEG image (left), Bounding box annotation visualization (right). diff --git a/examples/bbox_detection/data_annotated/2011_000003.jpg b/examples/bbox_detection/data_annotated/2011_000003.jpg new file mode 100755 index 0000000..b3bea66 Binary files /dev/null and b/examples/bbox_detection/data_annotated/2011_000003.jpg differ diff --git a/examples/bbox_detection/data_annotated/2011_000003.json b/examples/bbox_detection/data_annotated/2011_000003.json new file mode 100644 index 0000000..79c5f1e --- /dev/null +++ b/examples/bbox_detection/data_annotated/2011_000003.json @@ -0,0 +1,42 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 191.0, + 107.36900369003689 + ], + [ + 313.0, + 329.36900369003695 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 365.0, + 83.0 + ], + [ + 500.0, + 333.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + } + ], + "imagePath": "2011_000003.jpg", + "imageData": null, + "imageHeight": 338, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/bbox_detection/data_annotated/2011_000006.jpg b/examples/bbox_detection/data_annotated/2011_000006.jpg new file mode 100755 index 0000000..d713c46 Binary files /dev/null and b/examples/bbox_detection/data_annotated/2011_000006.jpg differ diff --git a/examples/bbox_detection/data_annotated/2011_000006.json b/examples/bbox_detection/data_annotated/2011_000006.json new file mode 100644 index 0000000..9289f10 --- /dev/null +++ b/examples/bbox_detection/data_annotated/2011_000006.json @@ -0,0 +1,74 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 91.0, + 107.0 + ], + [ + 240.0, + 330.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 178.0, + 110.0 + ], + [ + 298.0, + 282.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 254.38461538461536, + 115.38461538461539 + ], + [ + 369.38461538461536, + 292.38461538461536 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 395.0, + 81.0 + ], + [ + 447.0, + 117.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + } + ], + "imagePath": "2011_000006.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/bbox_detection/data_annotated/2011_000025.jpg b/examples/bbox_detection/data_annotated/2011_000025.jpg new file mode 100755 index 0000000..c26c389 Binary files /dev/null and b/examples/bbox_detection/data_annotated/2011_000025.jpg differ diff --git a/examples/bbox_detection/data_annotated/2011_000025.json b/examples/bbox_detection/data_annotated/2011_000025.json new file mode 100644 index 0000000..524db97 --- /dev/null +++ b/examples/bbox_detection/data_annotated/2011_000025.json @@ -0,0 +1,58 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "bus", + "points": [ + [ + 84.0, + 20.384615384615387 + ], + [ + 435.0, + 373.38461538461536 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "bus", + "points": [ + [ + 1.0, + 99.0 + ], + [ + 107.0, + 282.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 409.0, + 167.0 + ], + [ + 500.0, + 266.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + } + ], + "imagePath": "2011_000025.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/bbox_detection/data_dataset_voc/Annotations/2011_000003.xml b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000003.xml new file mode 100644 index 0000000..d67f885 --- /dev/null +++ b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000003.xml @@ -0,0 +1,37 @@ + + + 2011_000003.jpg + + + + + 338 + 500 + 3 + + + + person + + + + + 191.0 + 107.36900369003689 + 313.0 + 329.36900369003695 + + + + person + + + + + 365.0 + 83.0 + 500.0 + 333.0 + + + diff --git a/examples/bbox_detection/data_dataset_voc/Annotations/2011_000006.xml b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000006.xml new file mode 100644 index 0000000..555977c --- /dev/null +++ b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000006.xml @@ -0,0 +1,61 @@ + + + 2011_000006.jpg + + + + + 375 + 500 + 3 + + + + person + + + + + 91.0 + 107.0 + 240.0 + 330.0 + + + + person + + + + + 178.0 + 110.0 + 298.0 + 282.0 + + + + person + + + + + 254.38461538461536 + 115.38461538461539 + 369.38461538461536 + 292.38461538461536 + + + + person + + + + + 395.0 + 81.0 + 447.0 + 117.0 + + + diff --git a/examples/bbox_detection/data_dataset_voc/Annotations/2011_000025.xml b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000025.xml new file mode 100644 index 0000000..8849057 --- /dev/null +++ b/examples/bbox_detection/data_dataset_voc/Annotations/2011_000025.xml @@ -0,0 +1,49 @@ + + + 2011_000025.jpg + + + + + 375 + 500 + 3 + + + + bus + + + + + 84.0 + 20.384615384615387 + 435.0 + 373.38461538461536 + + + + bus + + + + + 1.0 + 99.0 + 107.0 + 282.0 + + + + car + + + + + 409.0 + 167.0 + 500.0 + 266.0 + + + diff --git a/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000003.jpg b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000003.jpg new file mode 100644 index 0000000..d3b0b5f Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000003.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000006.jpg b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000006.jpg new file mode 100644 index 0000000..a2d1682 Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000006.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000025.jpg b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000025.jpg new file mode 100644 index 0000000..77e684a Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/AnnotationsVisualization/2011_000025.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000003.jpg b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000003.jpg new file mode 100644 index 0000000..7d8306f Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000003.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000006.jpg b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000006.jpg new file mode 100644 index 0000000..0f1617f Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000006.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000025.jpg b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000025.jpg new file mode 100644 index 0000000..eeb9cfa Binary files /dev/null and b/examples/bbox_detection/data_dataset_voc/JPEGImages/2011_000025.jpg differ diff --git a/examples/bbox_detection/data_dataset_voc/class_names.txt b/examples/bbox_detection/data_dataset_voc/class_names.txt new file mode 100644 index 0000000..84cc9ed --- /dev/null +++ b/examples/bbox_detection/data_dataset_voc/class_names.txt @@ -0,0 +1,21 @@ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/bbox_detection/labelme2voc.py b/examples/bbox_detection/labelme2voc.py new file mode 100755 index 0000000..71ab3b4 --- /dev/null +++ b/examples/bbox_detection/labelme2voc.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import argparse +import glob +import os +import os.path as osp +import sys + +import imgviz + +import labelme + +try: + import lxml.builder + import lxml.etree +except ImportError: + print("Please install lxml:\n\n pip install lxml\n") + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("input_dir", help="input annotated directory") + parser.add_argument("output_dir", help="output dataset directory") + parser.add_argument("--labels", help="labels file", required=True) + parser.add_argument("--noviz", help="no visualization", action="store_true") + args = parser.parse_args() + + if osp.exists(args.output_dir): + print("Output directory already exists:", args.output_dir) + sys.exit(1) + os.makedirs(args.output_dir) + os.makedirs(osp.join(args.output_dir, "JPEGImages")) + os.makedirs(osp.join(args.output_dir, "Annotations")) + if not args.noviz: + os.makedirs(osp.join(args.output_dir, "AnnotationsVisualization")) + print("Creating dataset:", args.output_dir) + + class_names = [] + class_name_to_id = {} + for i, line in enumerate(open(args.labels).readlines()): + class_id = i - 1 # starts with -1 + class_name = line.strip() + class_name_to_id[class_name] = class_id + if class_id == -1: + assert class_name == "__ignore__" + continue + elif class_id == 0: + assert class_name == "_background_" + class_names.append(class_name) + class_names = tuple(class_names) + print("class_names:", class_names) + out_class_names_file = osp.join(args.output_dir, "class_names.txt") + with open(out_class_names_file, "w") as f: + f.writelines("\n".join(class_names)) + print("Saved class_names:", out_class_names_file) + + for filename in glob.glob(osp.join(args.input_dir, "*.json")): + print("Generating dataset from:", filename) + + label_file = labelme.LabelFile(filename=filename) + + base = osp.splitext(osp.basename(filename))[0] + out_img_file = osp.join(args.output_dir, "JPEGImages", base + ".jpg") + out_xml_file = osp.join(args.output_dir, "Annotations", base + ".xml") + if not args.noviz: + out_viz_file = osp.join( + args.output_dir, "AnnotationsVisualization", base + ".jpg" + ) + + img = labelme.utils.img_data_to_arr(label_file.imageData) + imgviz.io.imsave(out_img_file, img) + + maker = lxml.builder.ElementMaker() + xml = maker.annotation( + maker.folder(), + maker.filename(base + ".jpg"), + maker.database(), # e.g., The VOC2007 Database + maker.annotation(), # e.g., Pascal VOC2007 + maker.image(), # e.g., flickr + maker.size( + maker.height(str(img.shape[0])), + maker.width(str(img.shape[1])), + maker.depth(str(img.shape[2])), + ), + maker.segmented(), + ) + + bboxes = [] + labels = [] + for shape in label_file.shapes: + if shape["shape_type"] != "rectangle": + print( + "Skipping shape: label={label}, " "shape_type={shape_type}".format( + **shape + ) + ) + continue + + class_name = shape["label"] + class_id = class_names.index(class_name) + + (xmin, ymin), (xmax, ymax) = shape["points"] + # swap if min is larger than max. + xmin, xmax = sorted([xmin, xmax]) + ymin, ymax = sorted([ymin, ymax]) + + bboxes.append([ymin, xmin, ymax, xmax]) + labels.append(class_id) + + xml.append( + maker.object( + maker.name(shape["label"]), + maker.pose(), + maker.truncated(), + maker.difficult(), + maker.bndbox( + maker.xmin(str(xmin)), + maker.ymin(str(ymin)), + maker.xmax(str(xmax)), + maker.ymax(str(ymax)), + ), + ) + ) + + if not args.noviz: + captions = [class_names[label] for label in labels] + viz = imgviz.instances2rgb( + image=img, + labels=labels, + bboxes=bboxes, + captions=captions, + font_size=15, + ) + imgviz.io.imsave(out_viz_file, viz) + + with open(out_xml_file, "wb") as f: + f.write(lxml.etree.tostring(xml, pretty_print=True)) + + +if __name__ == "__main__": + main() diff --git a/examples/bbox_detection/labels.txt b/examples/bbox_detection/labels.txt new file mode 100644 index 0000000..40668df --- /dev/null +++ b/examples/bbox_detection/labels.txt @@ -0,0 +1,22 @@ +__ignore__ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/classification/.readme/annotation_cat.jpg b/examples/classification/.readme/annotation_cat.jpg new file mode 100644 index 0000000..b804849 Binary files /dev/null and b/examples/classification/.readme/annotation_cat.jpg differ diff --git a/examples/classification/.readme/annotation_dog.jpg b/examples/classification/.readme/annotation_dog.jpg new file mode 100644 index 0000000..983fd7d Binary files /dev/null and b/examples/classification/.readme/annotation_dog.jpg differ diff --git a/examples/classification/README.md b/examples/classification/README.md new file mode 100644 index 0000000..30b3d77 --- /dev/null +++ b/examples/classification/README.md @@ -0,0 +1,11 @@ +# Classification Example + + +## Usage + +```bash +labelme data_annotated --flags flags.txt --nodata +``` + + + diff --git a/examples/classification/data_annotated/0001.jpg b/examples/classification/data_annotated/0001.jpg new file mode 100644 index 0000000..7e791e9 Binary files /dev/null and b/examples/classification/data_annotated/0001.jpg differ diff --git a/examples/classification/data_annotated/0001.json b/examples/classification/data_annotated/0001.json new file mode 100644 index 0000000..cfeb396 --- /dev/null +++ b/examples/classification/data_annotated/0001.json @@ -0,0 +1,13 @@ +{ + "version": "4.0.0", + "flags": { + "__ignore__": false, + "cat": true, + "dog": false + }, + "shapes": [], + "imagePath": "0001.jpg", + "imageData": null, + "imageHeight": 480, + "imageWidth": 640 +} \ No newline at end of file diff --git a/examples/classification/data_annotated/0002.jpg b/examples/classification/data_annotated/0002.jpg new file mode 100644 index 0000000..eb96f3e Binary files /dev/null and b/examples/classification/data_annotated/0002.jpg differ diff --git a/examples/classification/data_annotated/0002.json b/examples/classification/data_annotated/0002.json new file mode 100644 index 0000000..2769064 --- /dev/null +++ b/examples/classification/data_annotated/0002.json @@ -0,0 +1,13 @@ +{ + "version": "4.0.0", + "flags": { + "__ignore__": false, + "cat": false, + "dog": true + }, + "shapes": [], + "imagePath": "0002.jpg", + "imageData": null, + "imageHeight": 480, + "imageWidth": 640 +} \ No newline at end of file diff --git a/examples/classification/flags.txt b/examples/classification/flags.txt new file mode 100644 index 0000000..a80af6d --- /dev/null +++ b/examples/classification/flags.txt @@ -0,0 +1,3 @@ +__ignore__ +cat +dog diff --git a/examples/instance_segmentation/.readme/annotation.jpg b/examples/instance_segmentation/.readme/annotation.jpg new file mode 100644 index 0000000..ce44e50 Binary files /dev/null and b/examples/instance_segmentation/.readme/annotation.jpg differ diff --git a/examples/instance_segmentation/.readme/draw_label_png_class.jpg b/examples/instance_segmentation/.readme/draw_label_png_class.jpg new file mode 100644 index 0000000..3ff1b64 Binary files /dev/null and b/examples/instance_segmentation/.readme/draw_label_png_class.jpg differ diff --git a/examples/instance_segmentation/.readme/draw_label_png_object.jpg b/examples/instance_segmentation/.readme/draw_label_png_object.jpg new file mode 100644 index 0000000..b6228d8 Binary files /dev/null and b/examples/instance_segmentation/.readme/draw_label_png_object.jpg differ diff --git a/examples/instance_segmentation/README.md b/examples/instance_segmentation/README.md new file mode 100644 index 0000000..64a8f55 --- /dev/null +++ b/examples/instance_segmentation/README.md @@ -0,0 +1,49 @@ +# Instance Segmentation Example + +## Annotation + +```bash +labelme data_annotated --labels labels.txt --nodata --validatelabel exact --config '{shift_auto_shape_color: -2}' +labelme data_annotated --labels labels.txt --nodata --labelflags '{.*: [occluded, truncated], person: [male]}' +``` + +![](.readme/annotation.jpg) + +## Convert to VOC-format Dataset + +```bash +# It generates: +# - data_dataset_voc/JPEGImages +# - data_dataset_voc/SegmentationClass +# - data_dataset_voc/SegmentationClassNpy +# - data_dataset_voc/SegmentationClassVisualization +# - data_dataset_voc/SegmentationObject +# - data_dataset_voc/SegmentationObjectNpy +# - data_dataset_voc/SegmentationObjectVisualization +./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt +``` + + +Fig 1. JPEG image (left), JPEG class label visualization (center), JPEG instance label visualization (right) + + +Note that the label file contains only very low label values (ex. `0, 4, 14`), and +`255` indicates the `__ignore__` label value (`-1` in the npy file). +You can see the label PNG file by following. + +```bash +labelme_draw_label_png data_dataset_voc/SegmentationClass/2011_000003.png # left +labelme_draw_label_png data_dataset_voc/SegmentationObject/2011_000003.png # right +``` + + + + +## Convert to COCO-format Dataset + +```bash +# It generates: +# - data_dataset_coco/JPEGImages +# - data_dataset_coco/annotations.json +./labelme2coco.py data_annotated data_dataset_coco --labels labels.txt +``` diff --git a/examples/instance_segmentation/data_annotated/2011_000003.jpg b/examples/instance_segmentation/data_annotated/2011_000003.jpg new file mode 100755 index 0000000..b3bea66 Binary files /dev/null and b/examples/instance_segmentation/data_annotated/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_annotated/2011_000003.json b/examples/instance_segmentation/data_annotated/2011_000003.json new file mode 100644 index 0000000..ef32cc3 --- /dev/null +++ b/examples/instance_segmentation/data_annotated/2011_000003.json @@ -0,0 +1,478 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 250.8142292490119, + 107.33596837944665 + ], + [ + 229.8142292490119, + 119.33596837944665 + ], + [ + 221.8142292490119, + 135.33596837944665 + ], + [ + 223.8142292490119, + 148.33596837944665 + ], + [ + 217.8142292490119, + 161.33596837944665 + ], + [ + 202.8142292490119, + 168.33596837944665 + ], + [ + 192.8142292490119, + 200.33596837944665 + ], + [ + 194.8142292490119, + 222.33596837944665 + ], + [ + 199.8142292490119, + 227.33596837944665 + ], + [ + 191.8142292490119, + 234.33596837944665 + ], + [ + 197.8142292490119, + 264.3359683794467 + ], + [ + 213.8142292490119, + 295.3359683794467 + ], + [ + 214.8142292490119, + 320.3359683794467 + ], + [ + 221.8142292490119, + 327.3359683794467 + ], + [ + 235.8142292490119, + 326.3359683794467 + ], + [ + 240.8142292490119, + 323.3359683794467 + ], + [ + 235.8142292490119, + 298.3359683794467 + ], + [ + 238.8142292490119, + 287.3359683794467 + ], + [ + 234.8142292490119, + 268.3359683794467 + ], + [ + 257.81422924901193, + 258.3359683794467 + ], + [ + 264.81422924901193, + 264.3359683794467 + ], + [ + 256.81422924901193, + 273.3359683794467 + ], + [ + 259.81422924901193, + 282.3359683794467 + ], + [ + 284.81422924901193, + 288.3359683794467 + ], + [ + 297.81422924901193, + 278.3359683794467 + ], + [ + 288.81422924901193, + 270.3359683794467 + ], + [ + 281.81422924901193, + 270.3359683794467 + ], + [ + 283.81422924901193, + 264.3359683794467 + ], + [ + 292.81422924901193, + 261.3359683794467 + ], + [ + 308.81422924901193, + 236.33596837944665 + ], + [ + 313.81422924901193, + 217.33596837944665 + ], + [ + 309.81422924901193, + 208.33596837944665 + ], + [ + 312.81422924901193, + 202.33596837944665 + ], + [ + 308.81422924901193, + 185.33596837944665 + ], + [ + 291.81422924901193, + 173.33596837944665 + ], + [ + 269.81422924901193, + 159.33596837944665 + ], + [ + 261.81422924901193, + 154.33596837944665 + ], + [ + 264.81422924901193, + 142.33596837944665 + ], + [ + 273.81422924901193, + 137.33596837944665 + ], + [ + 278.81422924901193, + 130.33596837944665 + ], + [ + 270.81422924901193, + 121.33596837944665 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 482.81422924901193, + 87.18098682963114 + ], + [ + 468.81422924901193, + 92.18098682963114 + ], + [ + 460.81422924901193, + 112.18098682963114 + ], + [ + 460.81422924901193, + 129.18098682963114 + ], + [ + 444.81422924901193, + 139.18098682963114 + ], + [ + 419.81422924901193, + 155.18098682963114 + ], + [ + 410.81422924901193, + 165.18098682963114 + ], + [ + 403.81422924901193, + 170.18098682963114 + ], + [ + 394.81422924901193, + 172.18098682963114 + ], + [ + 386.81422924901193, + 170.18098682963114 + ], + [ + 386.81422924901193, + 186.18098682963114 + ], + [ + 392.81422924901193, + 184.18098682963114 + ], + [ + 410.81422924901193, + 189.18098682963114 + ], + [ + 414.81422924901193, + 194.18098682963114 + ], + [ + 437.81422924901193, + 191.18098682963114 + ], + [ + 434.81422924901193, + 206.18098682963114 + ], + [ + 390.81422924901193, + 197.18098682963114 + ], + [ + 386.81422924901193, + 197.18098682963114 + ], + [ + 387.81422924901193, + 210.18098682963114 + ], + [ + 381.81422924901193, + 214.18098682963114 + ], + [ + 372.81422924901193, + 214.18098682963114 + ], + [ + 372.81422924901193, + 218.18098682963114 + ], + [ + 400.81422924901193, + 272.18098682963114 + ], + [ + 389.81422924901193, + 274.18098682963114 + ], + [ + 389.81422924901193, + 276.18098682963114 + ], + [ + 403.81422924901193, + 284.18098682963114 + ], + [ + 444.81422924901193, + 285.18098682963114 + ], + [ + 443.81422924901193, + 261.18098682963114 + ], + [ + 426.81422924901193, + 246.18098682963114 + ], + [ + 462.81422924901193, + 258.18098682963114 + ], + [ + 474.81422924901193, + 272.18098682963114 + ], + [ + 477.81422924901193, + 282.18098682963114 + ], + [ + 473.81422924901193, + 291.18098682963114 + ], + [ + 471.81422924901193, + 298.18098682963114 + ], + [ + 472.81422924901193, + 319.18098682963114 + ], + [ + 480.81422924901193, + 334.18098682963114 + ], + [ + 494.81422924901193, + 337.18098682963114 + ], + [ + 498.81422924901193, + 331.18098682963114 + ], + [ + 494.81422924901193, + 310.18098682963114 + ], + [ + 499.81422924901193, + 299.18098682963114 + ], + [ + 499.81422924901193, + 92.18098682963114 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 370.81422924901193, + 170.33596837944665 + ], + [ + 366.81422924901193, + 173.33596837944665 + ], + [ + 365.81422924901193, + 182.33596837944665 + ], + [ + 368.81422924901193, + 185.33596837944665 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "bottle", + "points": [ + [ + 374.81422924901193, + 159.33596837944665 + ], + [ + 369.81422924901193, + 170.33596837944665 + ], + [ + 369.81422924901193, + 210.33596837944665 + ], + [ + 375.81422924901193, + 212.33596837944665 + ], + [ + 387.81422924901193, + 209.33596837944665 + ], + [ + 385.81422924901193, + 185.33596837944665 + ], + [ + 385.81422924901193, + 168.33596837944665 + ], + [ + 385.81422924901193, + 165.33596837944665 + ], + [ + 382.81422924901193, + 159.33596837944665 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "__ignore__", + "points": [ + [ + 338.81422924901193, + 266.3359683794467 + ], + [ + 313.81422924901193, + 269.3359683794467 + ], + [ + 297.81422924901193, + 277.3359683794467 + ], + [ + 282.81422924901193, + 288.3359683794467 + ], + [ + 273.81422924901193, + 302.3359683794467 + ], + [ + 272.81422924901193, + 320.3359683794467 + ], + [ + 279.81422924901193, + 337.3359683794467 + ], + [ + 428.81422924901193, + 337.3359683794467 + ], + [ + 432.81422924901193, + 316.3359683794467 + ], + [ + 423.81422924901193, + 296.3359683794467 + ], + [ + 403.81422924901193, + 283.3359683794467 + ], + [ + 370.81422924901193, + 270.3359683794467 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000003.jpg", + "imageData": null, + "imageHeight": 338, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/instance_segmentation/data_annotated/2011_000006.jpg b/examples/instance_segmentation/data_annotated/2011_000006.jpg new file mode 100755 index 0000000..d713c46 Binary files /dev/null and b/examples/instance_segmentation/data_annotated/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_annotated/2011_000006.json b/examples/instance_segmentation/data_annotated/2011_000006.json new file mode 100644 index 0000000..dc9fa23 --- /dev/null +++ b/examples/instance_segmentation/data_annotated/2011_000006.json @@ -0,0 +1,530 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 204.936170212766, + 108.56382978723406 + ], + [ + 183.936170212766, + 141.56382978723406 + ], + [ + 166.936170212766, + 150.56382978723406 + ], + [ + 108.93617021276599, + 203.56382978723406 + ], + [ + 92.93617021276599, + 228.56382978723406 + ], + [ + 95.93617021276599, + 244.56382978723406 + ], + [ + 105.93617021276599, + 244.56382978723406 + ], + [ + 116.93617021276599, + 223.56382978723406 + ], + [ + 163.936170212766, + 187.56382978723406 + ], + [ + 147.936170212766, + 212.56382978723406 + ], + [ + 117.93617021276599, + 222.56382978723406 + ], + [ + 108.93617021276599, + 243.56382978723406 + ], + [ + 100.93617021276599, + 325.56382978723406 + ], + [ + 135.936170212766, + 329.56382978723406 + ], + [ + 148.936170212766, + 319.56382978723406 + ], + [ + 150.936170212766, + 295.56382978723406 + ], + [ + 169.936170212766, + 272.56382978723406 + ], + [ + 171.936170212766, + 249.56382978723406 + ], + [ + 178.936170212766, + 246.56382978723406 + ], + [ + 186.936170212766, + 225.56382978723406 + ], + [ + 214.936170212766, + 219.56382978723406 + ], + [ + 242.936170212766, + 157.56382978723406 + ], + [ + 228.936170212766, + 146.56382978723406 + ], + [ + 228.936170212766, + 125.56382978723406 + ], + [ + 216.936170212766, + 112.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 271.936170212766, + 109.56382978723406 + ], + [ + 249.936170212766, + 110.56382978723406 + ], + [ + 244.936170212766, + 150.56382978723406 + ], + [ + 215.936170212766, + 219.56382978723406 + ], + [ + 208.936170212766, + 245.56382978723406 + ], + [ + 214.936170212766, + 220.56382978723406 + ], + [ + 188.936170212766, + 227.56382978723406 + ], + [ + 170.936170212766, + 246.56382978723406 + ], + [ + 170.936170212766, + 275.56382978723406 + ], + [ + 221.936170212766, + 278.56382978723406 + ], + [ + 233.936170212766, + 259.56382978723406 + ], + [ + 246.936170212766, + 253.56382978723406 + ], + [ + 245.936170212766, + 256.56382978723406 + ], + [ + 242.936170212766, + 251.56382978723406 + ], + [ + 262.936170212766, + 256.56382978723406 + ], + [ + 304.936170212766, + 226.56382978723406 + ], + [ + 297.936170212766, + 199.56382978723406 + ], + [ + 308.936170212766, + 164.56382978723406 + ], + [ + 296.936170212766, + 148.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 308.936170212766, + 115.56382978723406 + ], + [ + 298.936170212766, + 145.56382978723406 + ], + [ + 309.936170212766, + 166.56382978723406 + ], + [ + 297.936170212766, + 200.56382978723406 + ], + [ + 305.936170212766, + 228.56382978723406 + ], + [ + 262.936170212766, + 258.56382978723406 + ], + [ + 252.936170212766, + 284.56382978723406 + ], + [ + 272.936170212766, + 291.56382978723406 + ], + [ + 281.936170212766, + 250.56382978723406 + ], + [ + 326.936170212766, + 235.56382978723406 + ], + [ + 351.936170212766, + 239.56382978723406 + ], + [ + 365.936170212766, + 223.56382978723406 + ], + [ + 371.936170212766, + 187.56382978723406 + ], + [ + 353.936170212766, + 168.56382978723406 + ], + [ + 344.936170212766, + 143.56382978723406 + ], + [ + 336.936170212766, + 115.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "chair", + "points": [ + [ + 309.7054009819968, + 242.94844517184941 + ], + [ + 282.7054009819968, + 251.94844517184941 + ], + [ + 271.7054009819968, + 287.9484451718494 + ], + [ + 175.70540098199677, + 275.9484451718494 + ], + [ + 149.70540098199677, + 296.9484451718494 + ], + [ + 151.70540098199677, + 319.9484451718494 + ], + [ + 160.70540098199677, + 328.9484451718494 + ], + [ + 165.54250204582655, + 375.38461538461536 + ], + [ + 486.7054009819968, + 373.9484451718494 + ], + [ + 498.7054009819968, + 336.9484451718494 + ], + [ + 498.7054009819968, + 202.94844517184941 + ], + [ + 454.7054009819968, + 193.94844517184941 + ], + [ + 435.7054009819968, + 212.94844517184941 + ], + [ + 368.7054009819968, + 224.94844517184941 + ], + [ + 351.7054009819968, + 241.94844517184941 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 425.936170212766, + 82.56382978723406 + ], + [ + 404.936170212766, + 109.56382978723406 + ], + [ + 400.936170212766, + 114.56382978723406 + ], + [ + 437.936170212766, + 114.56382978723406 + ], + [ + 448.936170212766, + 102.56382978723406 + ], + [ + 446.936170212766, + 91.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "__ignore__", + "points": [ + [ + 457.936170212766, + 85.56382978723406 + ], + [ + 439.936170212766, + 117.56382978723406 + ], + [ + 477.936170212766, + 117.56382978723406 + ], + [ + 474.936170212766, + 87.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 183.936170212766, + 140.56382978723406 + ], + [ + 125.93617021276599, + 140.56382978723406 + ], + [ + 110.93617021276599, + 187.56382978723406 + ], + [ + 22.936170212765987, + 199.56382978723406 + ], + [ + 18.936170212765987, + 218.56382978723406 + ], + [ + 22.936170212765987, + 234.56382978723406 + ], + [ + 93.93617021276599, + 239.56382978723406 + ], + [ + 91.93617021276599, + 229.56382978723406 + ], + [ + 110.93617021276599, + 203.56382978723406 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 103.93617021276599, + 290.56382978723406 + ], + [ + 58.93617021276599, + 303.56382978723406 + ], + [ + 97.93617021276599, + 311.56382978723406 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 348.936170212766, + 146.56382978723406 + ], + [ + 472.936170212766, + 149.56382978723406 + ], + [ + 477.936170212766, + 162.56382978723406 + ], + [ + 471.936170212766, + 196.56382978723406 + ], + [ + 453.936170212766, + 192.56382978723406 + ], + [ + 434.936170212766, + 213.56382978723406 + ], + [ + 368.936170212766, + 226.56382978723406 + ], + [ + 375.936170212766, + 187.56382978723406 + ], + [ + 353.936170212766, + 164.56382978723406 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 246.936170212766, + 252.56382978723406 + ], + [ + 219.936170212766, + 277.56382978723406 + ], + [ + 254.936170212766, + 287.56382978723406 + ], + [ + 261.936170212766, + 256.56382978723406 + ] + ], + "group_id": 0, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000006.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/instance_segmentation/data_annotated/2011_000025.jpg b/examples/instance_segmentation/data_annotated/2011_000025.jpg new file mode 100755 index 0000000..c26c389 Binary files /dev/null and b/examples/instance_segmentation/data_annotated/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_annotated/2011_000025.json b/examples/instance_segmentation/data_annotated/2011_000025.json new file mode 100644 index 0000000..8f5e615 --- /dev/null +++ b/examples/instance_segmentation/data_annotated/2011_000025.json @@ -0,0 +1,210 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "bus", + "points": [ + [ + 260.936170212766, + 23.33306055646483 + ], + [ + 193.936170212766, + 20.33306055646483 + ], + [ + 124.93617021276599, + 40.33306055646483 + ], + [ + 89.93617021276599, + 102.33306055646483 + ], + [ + 81.93617021276599, + 151.33306055646483 + ], + [ + 108.93617021276599, + 146.33306055646483 + ], + [ + 88.93617021276599, + 245.33306055646483 + ], + [ + 89.93617021276599, + 323.33306055646483 + ], + [ + 116.93617021276599, + 368.33306055646483 + ], + [ + 158.936170212766, + 369.33306055646483 + ], + [ + 165.936170212766, + 338.33306055646483 + ], + [ + 347.936170212766, + 336.33306055646483 + ], + [ + 349.936170212766, + 370.33306055646483 + ], + [ + 391.936170212766, + 374.33306055646483 + ], + [ + 403.936170212766, + 336.33306055646483 + ], + [ + 425.936170212766, + 333.33306055646483 + ], + [ + 421.936170212766, + 282.33306055646483 + ], + [ + 428.936170212766, + 253.33306055646483 + ], + [ + 428.936170212766, + 237.33306055646483 + ], + [ + 409.936170212766, + 221.33306055646483 + ], + [ + 409.936170212766, + 151.33306055646483 + ], + [ + 430.936170212766, + 144.33306055646483 + ], + [ + 433.936170212766, + 113.33306055646483 + ], + [ + 431.936170212766, + 97.33306055646483 + ], + [ + 408.936170212766, + 91.33306055646483 + ], + [ + 395.936170212766, + 51.33306055646483 + ], + [ + 338.936170212766, + 26.33306055646483 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "bus", + "points": [ + [ + 88.93617021276599, + 115.56382978723406 + ], + [ + 0.9361702127659877, + 96.56382978723406 + ], + [ + 0.0, + 251.968085106388 + ], + [ + 0.9361702127659877, + 265.56382978723406 + ], + [ + 27.936170212765987, + 265.56382978723406 + ], + [ + 29.936170212765987, + 283.56382978723406 + ], + [ + 63.93617021276599, + 281.56382978723406 + ], + [ + 89.93617021276599, + 252.56382978723406 + ], + [ + 100.93617021276599, + 183.56382978723406 + ], + [ + 108.93617021276599, + 145.56382978723406 + ], + [ + 81.93617021276599, + 151.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 413.936170212766, + 168.94844517184944 + ], + [ + 497.936170212766, + 168.94844517184944 + ], + [ + 497.936170212766, + 256.94844517184936 + ], + [ + 431.936170212766, + 258.94844517184936 + ], + [ + 430.936170212766, + 236.94844517184944 + ], + [ + 408.936170212766, + 218.94844517184944 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000025.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000003.jpg b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000003.jpg new file mode 100644 index 0000000..7d8306f Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000006.jpg b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000006.jpg new file mode 100644 index 0000000..0f1617f Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000025.jpg b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000025.jpg new file mode 100644 index 0000000..eeb9cfa Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/JPEGImages/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000003.jpg b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000003.jpg new file mode 100644 index 0000000..7ea6238 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000006.jpg b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000006.jpg new file mode 100644 index 0000000..4fcd3c7 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000025.jpg b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000025.jpg new file mode 100644 index 0000000..b78ba51 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_coco/Visualization/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_dataset_coco/annotations.json b/examples/instance_segmentation/data_dataset_coco/annotations.json new file mode 100644 index 0000000..f6a62fa --- /dev/null +++ b/examples/instance_segmentation/data_dataset_coco/annotations.json @@ -0,0 +1 @@ +{"info": {"description": null, "url": null, "version": null, "year": 2020, "contributor": null, "date_created": "2020-01-26 05:46:30.244442"}, "licenses": [{"url": null, "id": 0, "name": null}], "images": [{"license": 0, "url": null, "file_name": "JPEGImages/2011_000003.jpg", "height": 338, "width": 500, "date_captured": null, "id": 0}, {"license": 0, "url": null, "file_name": "JPEGImages/2011_000025.jpg", "height": 375, "width": 500, "date_captured": null, "id": 1}, {"license": 0, "url": null, "file_name": "JPEGImages/2011_000006.jpg", "height": 375, "width": 500, "date_captured": null, "id": 2}], "type": "instances", "annotations": [{"id": 0, "image_id": 0, "category_id": 15, "segmentation": [[250.8142292490119, 107.33596837944665, 229.8142292490119, 119.33596837944665, 221.8142292490119, 135.33596837944665, 223.8142292490119, 148.33596837944665, 217.8142292490119, 161.33596837944665, 202.8142292490119, 168.33596837944665, 192.8142292490119, 200.33596837944665, 194.8142292490119, 222.33596837944665, 199.8142292490119, 227.33596837944665, 191.8142292490119, 234.33596837944665, 197.8142292490119, 264.3359683794467, 213.8142292490119, 295.3359683794467, 214.8142292490119, 320.3359683794467, 221.8142292490119, 327.3359683794467, 235.8142292490119, 326.3359683794467, 240.8142292490119, 323.3359683794467, 235.8142292490119, 298.3359683794467, 238.8142292490119, 287.3359683794467, 234.8142292490119, 268.3359683794467, 257.81422924901193, 258.3359683794467, 264.81422924901193, 264.3359683794467, 256.81422924901193, 273.3359683794467, 259.81422924901193, 282.3359683794467, 284.81422924901193, 288.3359683794467, 297.81422924901193, 278.3359683794467, 288.81422924901193, 270.3359683794467, 281.81422924901193, 270.3359683794467, 283.81422924901193, 264.3359683794467, 292.81422924901193, 261.3359683794467, 308.81422924901193, 236.33596837944665, 313.81422924901193, 217.33596837944665, 309.81422924901193, 208.33596837944665, 312.81422924901193, 202.33596837944665, 308.81422924901193, 185.33596837944665, 291.81422924901193, 173.33596837944665, 269.81422924901193, 159.33596837944665, 261.81422924901193, 154.33596837944665, 264.81422924901193, 142.33596837944665, 273.81422924901193, 137.33596837944665, 278.81422924901193, 130.33596837944665, 270.81422924901193, 121.33596837944665]], "area": 15689.0, "bbox": [191.0, 107.0, 123.0, 221.0], "iscrowd": 0}, {"id": 1, "image_id": 0, "category_id": 15, "segmentation": [[482.81422924901193, 87.18098682963114, 468.81422924901193, 92.18098682963114, 460.81422924901193, 112.18098682963114, 460.81422924901193, 129.18098682963114, 444.81422924901193, 139.18098682963114, 419.81422924901193, 155.18098682963114, 410.81422924901193, 165.18098682963114, 403.81422924901193, 170.18098682963114, 394.81422924901193, 172.18098682963114, 386.81422924901193, 170.18098682963114, 386.81422924901193, 186.18098682963114, 392.81422924901193, 184.18098682963114, 410.81422924901193, 189.18098682963114, 414.81422924901193, 194.18098682963114, 437.81422924901193, 191.18098682963114, 434.81422924901193, 206.18098682963114, 390.81422924901193, 197.18098682963114, 386.81422924901193, 197.18098682963114, 387.81422924901193, 210.18098682963114, 381.81422924901193, 214.18098682963114, 372.81422924901193, 214.18098682963114, 372.81422924901193, 218.18098682963114, 400.81422924901193, 272.18098682963114, 389.81422924901193, 274.18098682963114, 389.81422924901193, 276.18098682963114, 403.81422924901193, 284.18098682963114, 444.81422924901193, 285.18098682963114, 443.81422924901193, 261.18098682963114, 426.81422924901193, 246.18098682963114, 462.81422924901193, 258.18098682963114, 474.81422924901193, 272.18098682963114, 477.81422924901193, 282.18098682963114, 473.81422924901193, 291.18098682963114, 471.81422924901193, 298.18098682963114, 472.81422924901193, 319.18098682963114, 480.81422924901193, 334.18098682963114, 494.81422924901193, 337.18098682963114, 498.81422924901193, 331.18098682963114, 494.81422924901193, 310.18098682963114, 499.81422924901193, 299.18098682963114, 499.81422924901193, 92.18098682963114], [370.81422924901193, 170.33596837944665, 366.81422924901193, 173.33596837944665, 365.81422924901193, 182.33596837944665, 368.81422924901193, 185.33596837944665]], "area": 17254.0, "bbox": [365.0, 87.0, 135.0, 251.0], "iscrowd": 0}, {"id": 2, "image_id": 0, "category_id": 5, "segmentation": [[374.81422924901193, 159.33596837944665, 369.81422924901193, 170.33596837944665, 369.81422924901193, 210.33596837944665, 375.81422924901193, 212.33596837944665, 387.81422924901193, 209.33596837944665, 385.81422924901193, 185.33596837944665, 385.81422924901193, 168.33596837944665, 385.81422924901193, 165.33596837944665, 382.81422924901193, 159.33596837944665]], "area": 873.0, "bbox": [369.0, 159.0, 19.0, 54.0], "iscrowd": 0}, {"id": 3, "image_id": 1, "category_id": 6, "segmentation": [[260.936170212766, 23.33306055646483, 193.936170212766, 20.33306055646483, 124.93617021276599, 40.33306055646483, 89.93617021276599, 102.33306055646483, 81.93617021276599, 151.33306055646483, 108.93617021276599, 146.33306055646483, 88.93617021276599, 245.33306055646483, 89.93617021276599, 323.33306055646483, 116.93617021276599, 368.33306055646483, 158.936170212766, 369.33306055646483, 165.936170212766, 338.33306055646483, 347.936170212766, 336.33306055646483, 349.936170212766, 370.33306055646483, 391.936170212766, 374.33306055646483, 403.936170212766, 336.33306055646483, 425.936170212766, 333.33306055646483, 421.936170212766, 282.33306055646483, 428.936170212766, 253.33306055646483, 428.936170212766, 237.33306055646483, 409.936170212766, 221.33306055646483, 409.936170212766, 151.33306055646483, 430.936170212766, 144.33306055646483, 433.936170212766, 113.33306055646483, 431.936170212766, 97.33306055646483, 408.936170212766, 91.33306055646483, 395.936170212766, 51.33306055646483, 338.936170212766, 26.33306055646483]], "area": 102701.0, "bbox": [81.0, 20.0, 353.0, 355.0], "iscrowd": 0}, {"id": 4, "image_id": 1, "category_id": 6, "segmentation": [[88.93617021276599, 115.56382978723406, 0.9361702127659877, 96.56382978723406, 0.0, 251.968085106388, 0.9361702127659877, 265.56382978723406, 27.936170212765987, 265.56382978723406, 29.936170212765987, 283.56382978723406, 63.93617021276599, 281.56382978723406, 89.93617021276599, 252.56382978723406, 100.93617021276599, 183.56382978723406, 108.93617021276599, 145.56382978723406, 81.93617021276599, 151.56382978723406]], "area": 15781.0, "bbox": [0.0, 96.0, 109.0, 188.0], "iscrowd": 0}, {"id": 5, "image_id": 1, "category_id": 7, "segmentation": [[413.936170212766, 168.94844517184944, 497.936170212766, 168.94844517184944, 497.936170212766, 256.94844517184936, 431.936170212766, 258.94844517184936, 430.936170212766, 236.94844517184944, 408.936170212766, 218.94844517184944]], "area": 7256.0, "bbox": [408.0, 168.0, 90.0, 91.0], "iscrowd": 0}, {"id": 6, "image_id": 2, "category_id": 15, "segmentation": [[204.936170212766, 108.56382978723406, 183.936170212766, 141.56382978723406, 166.936170212766, 150.56382978723406, 108.93617021276599, 203.56382978723406, 92.93617021276599, 228.56382978723406, 95.93617021276599, 244.56382978723406, 105.93617021276599, 244.56382978723406, 116.93617021276599, 223.56382978723406, 163.936170212766, 187.56382978723406, 147.936170212766, 212.56382978723406, 117.93617021276599, 222.56382978723406, 108.93617021276599, 243.56382978723406, 100.93617021276599, 325.56382978723406, 135.936170212766, 329.56382978723406, 148.936170212766, 319.56382978723406, 150.936170212766, 295.56382978723406, 169.936170212766, 272.56382978723406, 171.936170212766, 249.56382978723406, 178.936170212766, 246.56382978723406, 186.936170212766, 225.56382978723406, 214.936170212766, 219.56382978723406, 242.936170212766, 157.56382978723406, 228.936170212766, 146.56382978723406, 228.936170212766, 125.56382978723406, 216.936170212766, 112.56382978723406]], "area": 15203.0, "bbox": [92.0, 108.0, 151.0, 222.0], "iscrowd": 0}, {"id": 7, "image_id": 2, "category_id": 15, "segmentation": [[271.936170212766, 109.56382978723406, 249.936170212766, 110.56382978723406, 244.936170212766, 150.56382978723406, 215.936170212766, 219.56382978723406, 208.936170212766, 245.56382978723406, 214.936170212766, 220.56382978723406, 188.936170212766, 227.56382978723406, 170.936170212766, 246.56382978723406, 170.936170212766, 275.56382978723406, 221.936170212766, 278.56382978723406, 233.936170212766, 259.56382978723406, 246.936170212766, 253.56382978723406, 245.936170212766, 256.56382978723406, 242.936170212766, 251.56382978723406, 262.936170212766, 256.56382978723406, 304.936170212766, 226.56382978723406, 297.936170212766, 199.56382978723406, 308.936170212766, 164.56382978723406, 296.936170212766, 148.56382978723406]], "area": 11735.0, "bbox": [170.0, 109.0, 139.0, 170.0], "iscrowd": 0}, {"id": 8, "image_id": 2, "category_id": 15, "segmentation": [[308.936170212766, 115.56382978723406, 298.936170212766, 145.56382978723406, 309.936170212766, 166.56382978723406, 297.936170212766, 200.56382978723406, 305.936170212766, 228.56382978723406, 262.936170212766, 258.56382978723406, 252.936170212766, 284.56382978723406, 272.936170212766, 291.56382978723406, 281.936170212766, 250.56382978723406, 326.936170212766, 235.56382978723406, 351.936170212766, 239.56382978723406, 365.936170212766, 223.56382978723406, 371.936170212766, 187.56382978723406, 353.936170212766, 168.56382978723406, 344.936170212766, 143.56382978723406, 336.936170212766, 115.56382978723406]], "area": 7597.0, "bbox": [252.0, 115.0, 120.0, 177.0], "iscrowd": 0}, {"id": 9, "image_id": 2, "category_id": 9, "segmentation": [[309.7054009819968, 242.94844517184941, 282.7054009819968, 251.94844517184941, 271.7054009819968, 287.9484451718494, 175.70540098199677, 275.9484451718494, 149.70540098199677, 296.9484451718494, 151.70540098199677, 319.9484451718494, 160.70540098199677, 328.9484451718494, 165.54250204582655, 375.38461538461536, 486.7054009819968, 373.9484451718494, 498.7054009819968, 336.9484451718494, 498.7054009819968, 202.94844517184941, 454.7054009819968, 193.94844517184941, 435.7054009819968, 212.94844517184941, 368.7054009819968, 224.94844517184941, 351.7054009819968, 241.94844517184941]], "area": 44532.0, "bbox": [149.0, 193.0, 350.0, 182.0], "iscrowd": 0}, {"id": 10, "image_id": 2, "category_id": 15, "segmentation": [[425.936170212766, 82.56382978723406, 404.936170212766, 109.56382978723406, 400.936170212766, 114.56382978723406, 437.936170212766, 114.56382978723406, 448.936170212766, 102.56382978723406, 446.936170212766, 91.56382978723406]], "area": 996.0, "bbox": [400.0, 82.0, 49.0, 33.0], "iscrowd": 0}, {"id": 11, "image_id": 2, "category_id": 18, "segmentation": [[183.936170212766, 140.56382978723406, 125.93617021276599, 140.56382978723406, 110.93617021276599, 187.56382978723406, 22.936170212765987, 199.56382978723406, 18.936170212765987, 218.56382978723406, 22.936170212765987, 234.56382978723406, 93.93617021276599, 239.56382978723406, 91.93617021276599, 229.56382978723406, 110.93617021276599, 203.56382978723406], [103.93617021276599, 290.56382978723406, 58.93617021276599, 303.56382978723406, 97.93617021276599, 311.56382978723406], [348.936170212766, 146.56382978723406, 472.936170212766, 149.56382978723406, 477.936170212766, 162.56382978723406, 471.936170212766, 196.56382978723406, 453.936170212766, 192.56382978723406, 434.936170212766, 213.56382978723406, 368.936170212766, 226.56382978723406, 375.936170212766, 187.56382978723406, 353.936170212766, 164.56382978723406], [246.936170212766, 252.56382978723406, 219.936170212766, 277.56382978723406, 254.936170212766, 287.56382978723406, 261.936170212766, 256.56382978723406]], "area": 14001.0, "bbox": [18.0, 140.0, 460.0, 172.0], "iscrowd": 0}], "categories": [{"supercategory": null, "id": 0, "name": "_background_"}, {"supercategory": null, "id": 1, "name": "aeroplane"}, {"supercategory": null, "id": 2, "name": "bicycle"}, {"supercategory": null, "id": 3, "name": "bird"}, {"supercategory": null, "id": 4, "name": "boat"}, {"supercategory": null, "id": 5, "name": "bottle"}, {"supercategory": null, "id": 6, "name": "bus"}, {"supercategory": null, "id": 7, "name": "car"}, {"supercategory": null, "id": 8, "name": "cat"}, {"supercategory": null, "id": 9, "name": "chair"}, {"supercategory": null, "id": 10, "name": "cow"}, {"supercategory": null, "id": 11, "name": "diningtable"}, {"supercategory": null, "id": 12, "name": "dog"}, {"supercategory": null, "id": 13, "name": "horse"}, {"supercategory": null, "id": 14, "name": "motorbike"}, {"supercategory": null, "id": 15, "name": "person"}, {"supercategory": null, "id": 16, "name": "potted plant"}, {"supercategory": null, "id": 17, "name": "sheep"}, {"supercategory": null, "id": 18, "name": "sofa"}, {"supercategory": null, "id": 19, "name": "train"}, {"supercategory": null, "id": 20, "name": "tv/monitor"}]} \ No newline at end of file diff --git a/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg new file mode 100644 index 0000000..6049149 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg new file mode 100644 index 0000000..ff71ad4 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg new file mode 100644 index 0000000..df880f4 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png new file mode 100644 index 0000000..6de7cef Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png new file mode 100644 index 0000000..6e60e4f Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png new file mode 100644 index 0000000..7d09bea Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy new file mode 100644 index 0000000..a8f2d1f Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy new file mode 100644 index 0000000..674c104 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy new file mode 100644 index 0000000..b327043 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg new file mode 100644 index 0000000..a0ac5af Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg new file mode 100644 index 0000000..512efaf Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg new file mode 100644 index 0000000..097eef3 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000003.png b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000003.png new file mode 100644 index 0000000..567b9ab Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000003.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000006.png b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000006.png new file mode 100644 index 0000000..7fbae14 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000006.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000025.png b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000025.png new file mode 100644 index 0000000..7285291 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObject/2011_000025.png differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000003.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000003.npy new file mode 100644 index 0000000..eb76e49 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000003.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000006.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000006.npy new file mode 100644 index 0000000..54cf658 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000006.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000025.npy b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000025.npy new file mode 100644 index 0000000..de4d8a1 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectNpy/2011_000025.npy differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000003.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000003.jpg new file mode 100644 index 0000000..df3b412 Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000003.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000006.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000006.jpg new file mode 100644 index 0000000..13b74dc Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000006.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000025.jpg b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000025.jpg new file mode 100644 index 0000000..56c8a7a Binary files /dev/null and b/examples/instance_segmentation/data_dataset_voc/SegmentationObjectVisualization/2011_000025.jpg differ diff --git a/examples/instance_segmentation/data_dataset_voc/class_names.txt b/examples/instance_segmentation/data_dataset_voc/class_names.txt new file mode 100644 index 0000000..84cc9ed --- /dev/null +++ b/examples/instance_segmentation/data_dataset_voc/class_names.txt @@ -0,0 +1,21 @@ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/instance_segmentation/labelme2coco.py b/examples/instance_segmentation/labelme2coco.py new file mode 100755 index 0000000..0c5b4c3 --- /dev/null +++ b/examples/instance_segmentation/labelme2coco.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +import argparse +import collections +import datetime +import glob +import json +import os +import os.path as osp +import sys +import uuid + +import imgviz +import numpy as np + +import labelme + +try: + import pycocotools.mask +except ImportError: + print("Please install pycocotools:\n\n pip install pycocotools\n") + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("input_dir", help="input annotated directory") + parser.add_argument("output_dir", help="output dataset directory") + parser.add_argument("--labels", help="labels file", required=True) + parser.add_argument("--noviz", help="no visualization", action="store_true") + args = parser.parse_args() + + if osp.exists(args.output_dir): + print("Output directory already exists:", args.output_dir) + sys.exit(1) + os.makedirs(args.output_dir) + os.makedirs(osp.join(args.output_dir, "JPEGImages")) + if not args.noviz: + os.makedirs(osp.join(args.output_dir, "Visualization")) + print("Creating dataset:", args.output_dir) + + now = datetime.datetime.now() + + data = dict( + info=dict( + description=None, + url=None, + version=None, + year=now.year, + contributor=None, + date_created=now.strftime("%Y-%m-%d %H:%M:%S.%f"), + ), + licenses=[ + dict( + url=None, + id=0, + name=None, + ) + ], + images=[ + # license, url, file_name, height, width, date_captured, id + ], + type="instances", + annotations=[ + # segmentation, area, iscrowd, image_id, bbox, category_id, id + ], + categories=[ + # supercategory, id, name + ], + ) + + class_name_to_id = {} + for i, line in enumerate(open(args.labels).readlines()): + class_id = i - 1 # starts with -1 + class_name = line.strip() + if class_id == -1: + assert class_name == "__ignore__" + continue + class_name_to_id[class_name] = class_id + data["categories"].append( + dict( + supercategory=None, + id=class_id, + name=class_name, + ) + ) + + out_ann_file = osp.join(args.output_dir, "annotations.json") + label_files = glob.glob(osp.join(args.input_dir, "*.json")) + for image_id, filename in enumerate(label_files): + print("Generating dataset from:", filename) + + label_file = labelme.LabelFile(filename=filename) + + base = osp.splitext(osp.basename(filename))[0] + out_img_file = osp.join(args.output_dir, "JPEGImages", base + ".jpg") + + img = labelme.utils.img_data_to_arr(label_file.imageData) + imgviz.io.imsave(out_img_file, img) + data["images"].append( + dict( + license=0, + url=None, + file_name=osp.relpath(out_img_file, osp.dirname(out_ann_file)), + height=img.shape[0], + width=img.shape[1], + date_captured=None, + id=image_id, + ) + ) + + masks = {} # for area + segmentations = collections.defaultdict(list) # for segmentation + for shape in label_file.shapes: + points = shape["points"] + label = shape["label"] + group_id = shape.get("group_id") + shape_type = shape.get("shape_type", "polygon") + mask = labelme.utils.shape_to_mask(img.shape[:2], points, shape_type) + + if group_id is None: + group_id = uuid.uuid1() + + instance = (label, group_id) + + if instance in masks: + masks[instance] = masks[instance] | mask + else: + masks[instance] = mask + + if shape_type == "rectangle": + (x1, y1), (x2, y2) = points + x1, x2 = sorted([x1, x2]) + y1, y2 = sorted([y1, y2]) + points = [x1, y1, x2, y1, x2, y2, x1, y2] + if shape_type == "circle": + (x1, y1), (x2, y2) = points + r = np.linalg.norm([x2 - x1, y2 - y1]) + # r(1-cos(a/2)) N>pi/arccos(1-x/r) + # x: tolerance of the gap between the arc and the line segment + n_points_circle = max(int(np.pi / np.arccos(1 - 1 / r)), 12) + i = np.arange(n_points_circle) + x = x1 + r * np.sin(2 * np.pi / n_points_circle * i) + y = y1 + r * np.cos(2 * np.pi / n_points_circle * i) + points = np.stack((x, y), axis=1).flatten().tolist() + else: + points = np.asarray(points).flatten().tolist() + + segmentations[instance].append(points) + segmentations = dict(segmentations) + + for instance, mask in masks.items(): + cls_name, group_id = instance + if cls_name not in class_name_to_id: + continue + cls_id = class_name_to_id[cls_name] + + mask = np.asfortranarray(mask.astype(np.uint8)) + mask = pycocotools.mask.encode(mask) + area = float(pycocotools.mask.area(mask)) + bbox = pycocotools.mask.toBbox(mask).flatten().tolist() + + data["annotations"].append( + dict( + id=len(data["annotations"]), + image_id=image_id, + category_id=cls_id, + segmentation=segmentations[instance], + area=area, + bbox=bbox, + iscrowd=0, + ) + ) + + if not args.noviz: + viz = img + if masks: + labels, captions, masks = zip( + *[ + (class_name_to_id[cnm], cnm, msk) + for (cnm, gid), msk in masks.items() + if cnm in class_name_to_id + ] + ) + viz = imgviz.instances2rgb( + image=img, + labels=labels, + masks=masks, + captions=captions, + font_size=15, + line_width=2, + ) + out_viz_file = osp.join(args.output_dir, "Visualization", base + ".jpg") + imgviz.io.imsave(out_viz_file, viz) + + with open(out_ann_file, "w") as f: + json.dump(data, f) + + +if __name__ == "__main__": + main() diff --git a/examples/instance_segmentation/labelme2voc.py b/examples/instance_segmentation/labelme2voc.py new file mode 100755 index 0000000..c2edb7f --- /dev/null +++ b/examples/instance_segmentation/labelme2voc.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import argparse +import glob +import os +import os.path as osp +import sys + +import imgviz +import numpy as np + +import labelme + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("input_dir", help="Input annotated directory") + parser.add_argument("output_dir", help="Output dataset directory") + parser.add_argument( + "--labels", help="Labels file or comma separated text", required=True + ) + parser.add_argument( + "--noobject", help="Flag not to generate object label", action="store_true" + ) + parser.add_argument( + "--nonpy", help="Flag not to generate .npy files", action="store_true" + ) + parser.add_argument( + "--noviz", help="Flag to disable visualization", action="store_true" + ) + args = parser.parse_args() + + if osp.exists(args.output_dir): + print("Output directory already exists:", args.output_dir) + sys.exit(1) + os.makedirs(args.output_dir) + os.makedirs(osp.join(args.output_dir, "JPEGImages")) + os.makedirs(osp.join(args.output_dir, "SegmentationClass")) + if not args.nonpy: + os.makedirs(osp.join(args.output_dir, "SegmentationClassNpy")) + if not args.noviz: + os.makedirs(osp.join(args.output_dir, "SegmentationClassVisualization")) + if not args.noobject: + os.makedirs(osp.join(args.output_dir, "SegmentationObject")) + if not args.nonpy: + os.makedirs(osp.join(args.output_dir, "SegmentationObjectNpy")) + if not args.noviz: + os.makedirs(osp.join(args.output_dir, "SegmentationObjectVisualization")) + print("Creating dataset:", args.output_dir) + + if osp.exists(args.labels): + with open(args.labels) as f: + labels = [label.strip() for label in f if label] + else: + labels = [label.strip() for label in args.labels.split(",")] + + class_names = [] + class_name_to_id = {} + for i, label in enumerate(labels): + class_id = i - 1 # starts with -1 + class_name = label.strip() + class_name_to_id[class_name] = class_id + if class_id == -1: + assert class_name == "__ignore__" + continue + elif class_id == 0: + assert class_name == "_background_" + class_names.append(class_name) + class_names = tuple(class_names) + print("class_names:", class_names) + out_class_names_file = osp.join(args.output_dir, "class_names.txt") + with open(out_class_names_file, "w") as f: + f.writelines("\n".join(class_names)) + print("Saved class_names:", out_class_names_file) + + for filename in sorted(glob.glob(osp.join(args.input_dir, "*.json"))): + print("Generating dataset from:", filename) + + label_file = labelme.LabelFile(filename=filename) + + base = osp.splitext(osp.basename(filename))[0] + out_img_file = osp.join(args.output_dir, "JPEGImages", base + ".jpg") + out_clsp_file = osp.join(args.output_dir, "SegmentationClass", base + ".png") + if not args.nonpy: + out_cls_file = osp.join( + args.output_dir, "SegmentationClassNpy", base + ".npy" + ) + if not args.noviz: + out_clsv_file = osp.join( + args.output_dir, + "SegmentationClassVisualization", + base + ".jpg", + ) + if not args.noobject: + out_insp_file = osp.join( + args.output_dir, "SegmentationObject", base + ".png" + ) + if not args.nonpy: + out_ins_file = osp.join( + args.output_dir, "SegmentationObjectNpy", base + ".npy" + ) + if not args.noviz: + out_insv_file = osp.join( + args.output_dir, + "SegmentationObjectVisualization", + base + ".jpg", + ) + + img = labelme.utils.img_data_to_arr(label_file.imageData) + imgviz.io.imsave(out_img_file, img) + + cls, ins = labelme.utils.shapes_to_label( + img_shape=img.shape, + shapes=label_file.shapes, + label_name_to_value=class_name_to_id, + ) + ins[cls == -1] = 0 # ignore it. + + # class label + labelme.utils.lblsave(out_clsp_file, cls) + if not args.nonpy: + np.save(out_cls_file, cls) + if not args.noviz: + clsv = imgviz.label2rgb( + cls, + imgviz.rgb2gray(img), + label_names=class_names, + font_size=15, + loc="rb", + ) + imgviz.io.imsave(out_clsv_file, clsv) + + if not args.noobject: + # instance label + labelme.utils.lblsave(out_insp_file, ins) + if not args.nonpy: + np.save(out_ins_file, ins) + if not args.noviz: + instance_ids = np.unique(ins) + instance_names = [str(i) for i in range(max(instance_ids) + 1)] + insv = imgviz.label2rgb( + ins, + imgviz.rgb2gray(img), + label_names=instance_names, + font_size=15, + loc="rb", + ) + imgviz.io.imsave(out_insv_file, insv) + + +if __name__ == "__main__": + main() diff --git a/examples/instance_segmentation/labels.txt b/examples/instance_segmentation/labels.txt new file mode 100644 index 0000000..40668df --- /dev/null +++ b/examples/instance_segmentation/labels.txt @@ -0,0 +1,22 @@ +__ignore__ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/primitives/primitives.jpg b/examples/primitives/primitives.jpg new file mode 100644 index 0000000..9ed704a Binary files /dev/null and b/examples/primitives/primitives.jpg differ diff --git a/examples/primitives/primitives.json b/examples/primitives/primitives.json new file mode 100644 index 0000000..2df92d8 --- /dev/null +++ b/examples/primitives/primitives.json @@ -0,0 +1,130 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "rectangle", + "points": [ + [ + 32.0, + 35.0 + ], + [ + 132.0, + 135.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "circle", + "points": [ + [ + 195.0, + 84.0 + ], + [ + 225.0, + 125.0 + ] + ], + "group_id": null, + "shape_type": "circle", + "flags": {} + }, + { + "label": "rectangle", + "points": [ + [ + 391.0, + 33.0 + ], + [ + 542.0, + 135.0 + ] + ], + "group_id": null, + "shape_type": "rectangle", + "flags": {} + }, + { + "label": "polygon", + "points": [ + [ + 69.0, + 318.0 + ], + [ + 45.0, + 403.0 + ], + [ + 173.0, + 406.0 + ], + [ + 198.0, + 321.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "line", + "points": [ + [ + 188.0, + 178.0 + ], + [ + 160.0, + 224.0 + ] + ], + "group_id": null, + "shape_type": "line", + "flags": {} + }, + { + "label": "point", + "points": [ + [ + 345.0, + 174.0 + ] + ], + "group_id": null, + "shape_type": "point", + "flags": {} + }, + { + "label": "line_strip", + "points": [ + [ + 440.53703703703707, + 181.46296296296293 + ], + [ + 402.53703703703707, + 274.46296296296293 + ], + [ + 544.5370370370371, + 275.46296296296293 + ] + ], + "group_id": null, + "shape_type": "linestrip", + "flags": {} + } + ], + "imagePath": "primitives.jpg", + "imageData": null, + "imageHeight": 450, + "imageWidth": 560 +} \ No newline at end of file diff --git a/examples/semantic_segmentation/.readme/annotation.jpg b/examples/semantic_segmentation/.readme/annotation.jpg new file mode 100644 index 0000000..dd7dea6 Binary files /dev/null and b/examples/semantic_segmentation/.readme/annotation.jpg differ diff --git a/examples/semantic_segmentation/.readme/draw_label_png.jpg b/examples/semantic_segmentation/.readme/draw_label_png.jpg new file mode 100644 index 0000000..3ff1b64 Binary files /dev/null and b/examples/semantic_segmentation/.readme/draw_label_png.jpg differ diff --git a/examples/semantic_segmentation/README.md b/examples/semantic_segmentation/README.md new file mode 100644 index 0000000..c686361 --- /dev/null +++ b/examples/semantic_segmentation/README.md @@ -0,0 +1,36 @@ +# Semantic Segmentation Example + +## Annotation + +```bash +labelme data_annotated --labels labels.txt --nodata --validatelabel exact --config '{shift_auto_shape_color: -2}' +``` + +![](.readme/annotation.jpg) + + +## Convert to VOC-format Dataset + +```bash +# It generates: +# - data_dataset_voc/JPEGImages +# - data_dataset_voc/SegmentationClass +# - data_dataset_voc/SegmentationClassNpy +# - data_dataset_voc/SegmentationClassVisualization +./labelme2voc.py data_annotated data_dataset_voc --labels labels.txt --noobject +``` + + + +Fig 1. JPEG image (left), PNG label (center), JPEG label visualization (right) + + +Note that the label file contains only very low label values (ex. `0, 4, 14`), and +`255` indicates the `__ignore__` label value (`-1` in the npy file). +You can see the label PNG file by following. + +```bash +labelme_draw_label_png data_dataset_voc/SegmentationClass/2011_000003.png +``` + + diff --git a/examples/semantic_segmentation/data_annotated/2011_000003.jpg b/examples/semantic_segmentation/data_annotated/2011_000003.jpg new file mode 100755 index 0000000..b3bea66 Binary files /dev/null and b/examples/semantic_segmentation/data_annotated/2011_000003.jpg differ diff --git a/examples/semantic_segmentation/data_annotated/2011_000003.json b/examples/semantic_segmentation/data_annotated/2011_000003.json new file mode 100644 index 0000000..9797c7b --- /dev/null +++ b/examples/semantic_segmentation/data_annotated/2011_000003.json @@ -0,0 +1,478 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 250.8142292490119, + 106.96696468940974 + ], + [ + 229.8142292490119, + 118.96696468940974 + ], + [ + 221.8142292490119, + 134.96696468940974 + ], + [ + 223.8142292490119, + 147.96696468940974 + ], + [ + 217.8142292490119, + 160.96696468940974 + ], + [ + 202.8142292490119, + 167.96696468940974 + ], + [ + 192.8142292490119, + 199.96696468940974 + ], + [ + 194.8142292490119, + 221.96696468940974 + ], + [ + 199.8142292490119, + 226.96696468940974 + ], + [ + 191.8142292490119, + 233.96696468940974 + ], + [ + 197.8142292490119, + 263.9669646894098 + ], + [ + 213.8142292490119, + 294.9669646894098 + ], + [ + 214.8142292490119, + 319.9669646894098 + ], + [ + 221.8142292490119, + 326.9669646894098 + ], + [ + 235.8142292490119, + 325.9669646894098 + ], + [ + 240.8142292490119, + 322.9669646894098 + ], + [ + 235.8142292490119, + 297.9669646894098 + ], + [ + 238.8142292490119, + 286.9669646894098 + ], + [ + 234.8142292490119, + 267.9669646894098 + ], + [ + 257.81422924901193, + 257.9669646894098 + ], + [ + 264.81422924901193, + 263.9669646894098 + ], + [ + 256.81422924901193, + 272.9669646894098 + ], + [ + 259.81422924901193, + 281.9669646894098 + ], + [ + 284.81422924901193, + 287.9669646894098 + ], + [ + 297.81422924901193, + 277.9669646894098 + ], + [ + 288.81422924901193, + 269.9669646894098 + ], + [ + 281.81422924901193, + 269.9669646894098 + ], + [ + 283.81422924901193, + 263.9669646894098 + ], + [ + 292.81422924901193, + 260.9669646894098 + ], + [ + 308.81422924901193, + 235.96696468940974 + ], + [ + 313.81422924901193, + 216.96696468940974 + ], + [ + 309.81422924901193, + 207.96696468940974 + ], + [ + 312.81422924901193, + 201.96696468940974 + ], + [ + 308.81422924901193, + 184.96696468940974 + ], + [ + 291.81422924901193, + 172.96696468940974 + ], + [ + 269.81422924901193, + 158.96696468940974 + ], + [ + 261.81422924901193, + 153.96696468940974 + ], + [ + 264.81422924901193, + 141.96696468940974 + ], + [ + 273.81422924901193, + 136.96696468940974 + ], + [ + 278.81422924901193, + 129.96696468940974 + ], + [ + 270.81422924901193, + 120.96696468940974 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 482.81422924901193, + 85.33596837944665 + ], + [ + 468.81422924901193, + 90.33596837944665 + ], + [ + 460.81422924901193, + 110.33596837944665 + ], + [ + 460.81422924901193, + 127.33596837944665 + ], + [ + 444.81422924901193, + 137.33596837944665 + ], + [ + 419.81422924901193, + 153.33596837944665 + ], + [ + 410.81422924901193, + 163.33596837944665 + ], + [ + 403.81422924901193, + 168.33596837944665 + ], + [ + 394.81422924901193, + 170.33596837944665 + ], + [ + 386.81422924901193, + 168.33596837944665 + ], + [ + 386.81422924901193, + 184.33596837944665 + ], + [ + 392.81422924901193, + 182.33596837944665 + ], + [ + 410.81422924901193, + 187.33596837944665 + ], + [ + 414.81422924901193, + 192.33596837944665 + ], + [ + 437.81422924901193, + 189.33596837944665 + ], + [ + 434.81422924901193, + 204.33596837944665 + ], + [ + 390.81422924901193, + 195.33596837944665 + ], + [ + 386.81422924901193, + 195.33596837944665 + ], + [ + 387.81422924901193, + 208.33596837944665 + ], + [ + 381.81422924901193, + 212.33596837944665 + ], + [ + 372.81422924901193, + 212.33596837944665 + ], + [ + 372.81422924901193, + 216.33596837944665 + ], + [ + 400.81422924901193, + 270.3359683794467 + ], + [ + 389.81422924901193, + 272.3359683794467 + ], + [ + 389.81422924901193, + 274.3359683794467 + ], + [ + 403.81422924901193, + 282.3359683794467 + ], + [ + 444.81422924901193, + 283.3359683794467 + ], + [ + 443.81422924901193, + 259.3359683794467 + ], + [ + 426.81422924901193, + 244.33596837944665 + ], + [ + 462.81422924901193, + 256.3359683794467 + ], + [ + 474.81422924901193, + 270.3359683794467 + ], + [ + 477.81422924901193, + 280.3359683794467 + ], + [ + 473.81422924901193, + 289.3359683794467 + ], + [ + 471.81422924901193, + 296.3359683794467 + ], + [ + 472.81422924901193, + 317.3359683794467 + ], + [ + 480.81422924901193, + 332.3359683794467 + ], + [ + 494.81422924901193, + 335.3359683794467 + ], + [ + 498.81422924901193, + 329.3359683794467 + ], + [ + 494.81422924901193, + 308.3359683794467 + ], + [ + 499.81422924901193, + 297.3359683794467 + ], + [ + 499.81422924901193, + 90.33596837944665 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 370.81422924901193, + 170.33596837944665 + ], + [ + 366.81422924901193, + 173.33596837944665 + ], + [ + 365.81422924901193, + 182.33596837944665 + ], + [ + 368.81422924901193, + 185.33596837944665 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "bottle", + "points": [ + [ + 374.81422924901193, + 159.33596837944665 + ], + [ + 369.81422924901193, + 170.33596837944665 + ], + [ + 369.81422924901193, + 210.33596837944665 + ], + [ + 375.81422924901193, + 212.33596837944665 + ], + [ + 387.81422924901193, + 209.33596837944665 + ], + [ + 385.81422924901193, + 185.33596837944665 + ], + [ + 385.81422924901193, + 168.33596837944665 + ], + [ + 385.81422924901193, + 165.33596837944665 + ], + [ + 382.81422924901193, + 159.33596837944665 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "__ignore__", + "points": [ + [ + 338.81422924901193, + 266.3359683794467 + ], + [ + 313.81422924901193, + 269.3359683794467 + ], + [ + 297.81422924901193, + 277.3359683794467 + ], + [ + 282.81422924901193, + 288.3359683794467 + ], + [ + 273.81422924901193, + 302.3359683794467 + ], + [ + 272.81422924901193, + 320.3359683794467 + ], + [ + 279.81422924901193, + 337.3359683794467 + ], + [ + 428.81422924901193, + 337.3359683794467 + ], + [ + 432.81422924901193, + 316.3359683794467 + ], + [ + 423.81422924901193, + 296.3359683794467 + ], + [ + 403.81422924901193, + 283.3359683794467 + ], + [ + 370.81422924901193, + 270.3359683794467 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000003.jpg", + "imageData": null, + "imageHeight": 338, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/semantic_segmentation/data_annotated/2011_000006.jpg b/examples/semantic_segmentation/data_annotated/2011_000006.jpg new file mode 100755 index 0000000..d713c46 Binary files /dev/null and b/examples/semantic_segmentation/data_annotated/2011_000006.jpg differ diff --git a/examples/semantic_segmentation/data_annotated/2011_000006.json b/examples/semantic_segmentation/data_annotated/2011_000006.json new file mode 100644 index 0000000..c036917 --- /dev/null +++ b/examples/semantic_segmentation/data_annotated/2011_000006.json @@ -0,0 +1,530 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "person", + "points": [ + [ + 204.936170212766, + 108.56382978723406 + ], + [ + 183.936170212766, + 141.56382978723406 + ], + [ + 166.936170212766, + 150.56382978723406 + ], + [ + 108.93617021276599, + 203.56382978723406 + ], + [ + 92.93617021276599, + 228.56382978723406 + ], + [ + 95.93617021276599, + 244.56382978723406 + ], + [ + 105.93617021276599, + 244.56382978723406 + ], + [ + 116.93617021276599, + 223.56382978723406 + ], + [ + 163.936170212766, + 187.56382978723406 + ], + [ + 147.936170212766, + 212.56382978723406 + ], + [ + 117.93617021276599, + 222.56382978723406 + ], + [ + 108.93617021276599, + 243.56382978723406 + ], + [ + 100.93617021276599, + 325.56382978723406 + ], + [ + 135.936170212766, + 329.56382978723406 + ], + [ + 148.936170212766, + 319.56382978723406 + ], + [ + 150.936170212766, + 295.56382978723406 + ], + [ + 169.936170212766, + 272.56382978723406 + ], + [ + 171.936170212766, + 249.56382978723406 + ], + [ + 178.936170212766, + 246.56382978723406 + ], + [ + 186.936170212766, + 225.56382978723406 + ], + [ + 214.936170212766, + 219.56382978723406 + ], + [ + 242.936170212766, + 157.56382978723406 + ], + [ + 228.936170212766, + 146.56382978723406 + ], + [ + 228.936170212766, + 125.56382978723406 + ], + [ + 216.936170212766, + 112.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 271.936170212766, + 109.56382978723406 + ], + [ + 249.936170212766, + 110.56382978723406 + ], + [ + 244.936170212766, + 150.56382978723406 + ], + [ + 215.936170212766, + 219.56382978723406 + ], + [ + 208.936170212766, + 245.56382978723406 + ], + [ + 214.936170212766, + 220.56382978723406 + ], + [ + 188.936170212766, + 227.56382978723406 + ], + [ + 170.936170212766, + 246.56382978723406 + ], + [ + 170.936170212766, + 275.56382978723406 + ], + [ + 221.936170212766, + 278.56382978723406 + ], + [ + 233.936170212766, + 259.56382978723406 + ], + [ + 246.936170212766, + 253.56382978723406 + ], + [ + 245.936170212766, + 256.56382978723406 + ], + [ + 242.936170212766, + 251.56382978723406 + ], + [ + 262.936170212766, + 256.56382978723406 + ], + [ + 304.936170212766, + 226.56382978723406 + ], + [ + 297.936170212766, + 199.56382978723406 + ], + [ + 308.936170212766, + 164.56382978723406 + ], + [ + 296.936170212766, + 148.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 308.936170212766, + 115.56382978723406 + ], + [ + 298.936170212766, + 145.56382978723406 + ], + [ + 309.936170212766, + 166.56382978723406 + ], + [ + 297.936170212766, + 200.56382978723406 + ], + [ + 305.936170212766, + 228.56382978723406 + ], + [ + 262.936170212766, + 258.56382978723406 + ], + [ + 252.936170212766, + 284.56382978723406 + ], + [ + 272.936170212766, + 291.56382978723406 + ], + [ + 281.936170212766, + 250.56382978723406 + ], + [ + 326.936170212766, + 235.56382978723406 + ], + [ + 351.936170212766, + 239.56382978723406 + ], + [ + 365.936170212766, + 223.56382978723406 + ], + [ + 371.936170212766, + 187.56382978723406 + ], + [ + 353.936170212766, + 168.56382978723406 + ], + [ + 344.936170212766, + 143.56382978723406 + ], + [ + 336.936170212766, + 115.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "chair", + "points": [ + [ + 308.936170212766, + 243.33306055646483 + ], + [ + 281.936170212766, + 252.33306055646483 + ], + [ + 270.936170212766, + 288.33306055646483 + ], + [ + 174.936170212766, + 276.33306055646483 + ], + [ + 148.936170212766, + 297.33306055646483 + ], + [ + 150.936170212766, + 320.33306055646483 + ], + [ + 159.936170212766, + 329.33306055646483 + ], + [ + 164.77327127659578, + 375.7692307692308 + ], + [ + 485.936170212766, + 374.33306055646483 + ], + [ + 497.936170212766, + 337.33306055646483 + ], + [ + 497.936170212766, + 203.33306055646483 + ], + [ + 453.936170212766, + 194.33306055646483 + ], + [ + 434.936170212766, + 213.33306055646483 + ], + [ + 367.936170212766, + 225.33306055646483 + ], + [ + 350.936170212766, + 242.33306055646483 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "person", + "points": [ + [ + 425.936170212766, + 82.56382978723406 + ], + [ + 404.936170212766, + 109.56382978723406 + ], + [ + 400.936170212766, + 114.56382978723406 + ], + [ + 437.936170212766, + 114.56382978723406 + ], + [ + 448.936170212766, + 102.56382978723406 + ], + [ + 446.936170212766, + 91.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "__ignore__", + "points": [ + [ + 457.936170212766, + 85.56382978723406 + ], + [ + 439.936170212766, + 117.56382978723406 + ], + [ + 477.936170212766, + 117.56382978723406 + ], + [ + 474.936170212766, + 87.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 183.936170212766, + 140.56382978723406 + ], + [ + 125.93617021276599, + 140.56382978723406 + ], + [ + 110.93617021276599, + 187.56382978723406 + ], + [ + 22.936170212765987, + 199.56382978723406 + ], + [ + 18.936170212765987, + 218.56382978723406 + ], + [ + 22.936170212765987, + 234.56382978723406 + ], + [ + 93.93617021276599, + 239.56382978723406 + ], + [ + 91.93617021276599, + 229.56382978723406 + ], + [ + 110.93617021276599, + 203.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 103.93617021276599, + 290.56382978723406 + ], + [ + 58.93617021276599, + 303.56382978723406 + ], + [ + 97.93617021276599, + 311.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 348.936170212766, + 146.56382978723406 + ], + [ + 472.936170212766, + 149.56382978723406 + ], + [ + 477.936170212766, + 162.56382978723406 + ], + [ + 471.936170212766, + 196.56382978723406 + ], + [ + 453.936170212766, + 192.56382978723406 + ], + [ + 434.936170212766, + 213.56382978723406 + ], + [ + 368.936170212766, + 226.56382978723406 + ], + [ + 375.936170212766, + 187.56382978723406 + ], + [ + 353.936170212766, + 164.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "sofa", + "points": [ + [ + 246.936170212766, + 252.56382978723406 + ], + [ + 219.936170212766, + 277.56382978723406 + ], + [ + 254.936170212766, + 287.56382978723406 + ], + [ + 261.936170212766, + 256.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000006.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/semantic_segmentation/data_annotated/2011_000025.jpg b/examples/semantic_segmentation/data_annotated/2011_000025.jpg new file mode 100755 index 0000000..c26c389 Binary files /dev/null and b/examples/semantic_segmentation/data_annotated/2011_000025.jpg differ diff --git a/examples/semantic_segmentation/data_annotated/2011_000025.json b/examples/semantic_segmentation/data_annotated/2011_000025.json new file mode 100644 index 0000000..1bd601f --- /dev/null +++ b/examples/semantic_segmentation/data_annotated/2011_000025.json @@ -0,0 +1,210 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "bus", + "points": [ + [ + 260.936170212766, + 22.948445171849443 + ], + [ + 193.936170212766, + 19.948445171849443 + ], + [ + 124.93617021276599, + 39.94844517184944 + ], + [ + 89.93617021276599, + 101.94844517184944 + ], + [ + 81.93617021276599, + 150.94844517184944 + ], + [ + 108.93617021276599, + 145.94844517184944 + ], + [ + 88.93617021276599, + 244.94844517184944 + ], + [ + 89.93617021276599, + 322.94844517184936 + ], + [ + 116.93617021276599, + 367.94844517184936 + ], + [ + 158.936170212766, + 368.94844517184936 + ], + [ + 165.936170212766, + 337.94844517184936 + ], + [ + 347.936170212766, + 335.94844517184936 + ], + [ + 349.936170212766, + 369.94844517184936 + ], + [ + 391.936170212766, + 373.94844517184936 + ], + [ + 403.936170212766, + 335.94844517184936 + ], + [ + 425.936170212766, + 332.94844517184936 + ], + [ + 421.936170212766, + 281.94844517184936 + ], + [ + 428.936170212766, + 252.94844517184944 + ], + [ + 428.936170212766, + 236.94844517184944 + ], + [ + 409.936170212766, + 220.94844517184944 + ], + [ + 409.936170212766, + 150.94844517184944 + ], + [ + 430.936170212766, + 143.94844517184944 + ], + [ + 433.936170212766, + 112.94844517184944 + ], + [ + 431.936170212766, + 96.94844517184944 + ], + [ + 408.936170212766, + 90.94844517184944 + ], + [ + 395.936170212766, + 50.94844517184944 + ], + [ + 338.936170212766, + 25.948445171849443 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "bus", + "points": [ + [ + 88.93617021276599, + 115.56382978723406 + ], + [ + 0.9361702127659877, + 96.56382978723406 + ], + [ + 0.0, + 251.968085106388 + ], + [ + 0.9361702127659877, + 265.56382978723406 + ], + [ + 27.936170212765987, + 265.56382978723406 + ], + [ + 29.936170212765987, + 283.56382978723406 + ], + [ + 63.93617021276599, + 281.56382978723406 + ], + [ + 89.93617021276599, + 252.56382978723406 + ], + [ + 100.93617021276599, + 183.56382978723406 + ], + [ + 108.93617021276599, + 145.56382978723406 + ], + [ + 81.93617021276599, + 151.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 413.936170212766, + 168.56382978723406 + ], + [ + 497.936170212766, + 168.56382978723406 + ], + [ + 497.936170212766, + 256.56382978723406 + ], + [ + 431.936170212766, + 258.56382978723406 + ], + [ + 430.936170212766, + 236.56382978723406 + ], + [ + 408.936170212766, + 218.56382978723406 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "2011_000025.jpg", + "imageData": null, + "imageHeight": 375, + "imageWidth": 500 +} \ No newline at end of file diff --git a/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg new file mode 100644 index 0000000..6049149 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000003.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg new file mode 100644 index 0000000..ff71ad4 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000006.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg new file mode 100644 index 0000000..df880f4 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/JPEGImages/2011_000025.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png new file mode 100644 index 0000000..820c94c Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000003.png differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png new file mode 100644 index 0000000..a60ce63 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000006.png differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png new file mode 100644 index 0000000..4b20820 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClass/2011_000025.png differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy new file mode 100644 index 0000000..dba09ba Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000003.npy differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy new file mode 100644 index 0000000..8aa911b Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000006.npy differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy new file mode 100644 index 0000000..8599dd1 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassNpy/2011_000025.npy differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg new file mode 100644 index 0000000..23827ef Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000003.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg new file mode 100644 index 0000000..2f98025 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000006.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg new file mode 100644 index 0000000..a5b85c0 Binary files /dev/null and b/examples/semantic_segmentation/data_dataset_voc/SegmentationClassVisualization/2011_000025.jpg differ diff --git a/examples/semantic_segmentation/data_dataset_voc/class_names.txt b/examples/semantic_segmentation/data_dataset_voc/class_names.txt new file mode 100644 index 0000000..84cc9ed --- /dev/null +++ b/examples/semantic_segmentation/data_dataset_voc/class_names.txt @@ -0,0 +1,21 @@ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/semantic_segmentation/labelme2voc.py b/examples/semantic_segmentation/labelme2voc.py new file mode 120000 index 0000000..8dd48d5 --- /dev/null +++ b/examples/semantic_segmentation/labelme2voc.py @@ -0,0 +1 @@ +../instance_segmentation/labelme2voc.py \ No newline at end of file diff --git a/examples/semantic_segmentation/labels.txt b/examples/semantic_segmentation/labels.txt new file mode 100644 index 0000000..40668df --- /dev/null +++ b/examples/semantic_segmentation/labels.txt @@ -0,0 +1,22 @@ +__ignore__ +_background_ +aeroplane +bicycle +bird +boat +bottle +bus +car +cat +chair +cow +diningtable +dog +horse +motorbike +person +potted plant +sheep +sofa +train +tv/monitor \ No newline at end of file diff --git a/examples/trackgui/TrackMe_overview.png b/examples/trackgui/TrackMe_overview.png new file mode 100644 index 0000000..bf96efe Binary files /dev/null and b/examples/trackgui/TrackMe_overview.png differ diff --git a/examples/tutorial/.readme/annotation.jpg b/examples/tutorial/.readme/annotation.jpg new file mode 100644 index 0000000..e365a52 Binary files /dev/null and b/examples/tutorial/.readme/annotation.jpg differ diff --git a/examples/tutorial/.readme/draw_json.jpg b/examples/tutorial/.readme/draw_json.jpg new file mode 100644 index 0000000..9f135a4 Binary files /dev/null and b/examples/tutorial/.readme/draw_json.jpg differ diff --git a/examples/tutorial/.readme/draw_label_png.jpg b/examples/tutorial/.readme/draw_label_png.jpg new file mode 100644 index 0000000..182dfd3 Binary files /dev/null and b/examples/tutorial/.readme/draw_label_png.jpg differ diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md new file mode 100644 index 0000000..2892850 --- /dev/null +++ b/examples/tutorial/README.md @@ -0,0 +1,66 @@ +# Tutorial (Single Image Example) + +## Annotation + +```bash +labelme apc2016_obj3.jpg -O apc2016_obj3.json +``` + +![](.readme/annotation.jpg) + + +## Visualization + +To view the json file quickly, you can use utility script: + +```bash +labelme_draw_json apc2016_obj3.json +``` + + + + +## Convert to Dataset + +To convert the json to set of image and label, you can run following: + + +```bash +labelme_export_json apc2016_obj3.json -o apc2016_obj3_json +``` + +It generates standard files from the JSON file. + +- [img.png](apc2016_obj3_json/img.png): Image file. +- [label.png](apc2016_obj3_json/label.png): uint8 label file. +- [label_viz.png](apc2016_obj3_json/label_viz.png): Visualization of `label.png`. +- [label_names.txt](apc2016_obj3_json/label_names.txt): Label names for values in `label.png`. + +## How to load label PNG file? + +Note that loading `label.png` is a bit difficult +(`scipy.misc.imread`, `skimage.io.imread` may not work correctly), +and please use `PIL.Image.open` to avoid unexpected behavior: + +```python +# see load_label_png.py also. +>>> import numpy as np +>>> import PIL.Image + +>>> label_png = 'apc2016_obj3_json/label.png' +>>> lbl = np.asarray(PIL.Image.open(label_png)) +>>> print(lbl.dtype) +dtype('uint8') +>>> np.unique(lbl) +array([0, 1, 2, 3], dtype=uint8) +>>> lbl.shape +(907, 1210) +``` + +Also, you can see the label PNG file by: + +```python +labelme_draw_label_png apc2016_obj3_json/label.png +``` + + diff --git a/examples/tutorial/apc2016_obj3.jpg b/examples/tutorial/apc2016_obj3.jpg new file mode 100644 index 0000000..dd9490a Binary files /dev/null and b/examples/tutorial/apc2016_obj3.jpg differ diff --git a/examples/tutorial/apc2016_obj3.json b/examples/tutorial/apc2016_obj3.json new file mode 100644 index 0000000..4d087ad --- /dev/null +++ b/examples/tutorial/apc2016_obj3.json @@ -0,0 +1,246 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "shelf", + "points": [ + [ + 7.942307692307736, + 80.76150251617551 + ], + [ + 171.94230769230774, + 714.7615025161755 + ], + [ + 968.9423076923077, + 733.7615025161755 + ], + [ + 1181.9423076923076, + 110.76150251617551 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "highland_6539_self_stick_notes", + "points": [ + [ + 430.16339869281046, + 516.2450980392157 + ], + [ + 390.9477124183006, + 606.4411764705883 + ], + [ + 398.7908496732026, + 697.2908496732026 + ], + [ + 522.3202614379085, + 711.6699346405229 + ], + [ + 762.18954248366, + 721.4738562091503 + ], + [ + 777.2222222222222, + 634.5457516339869 + ], + [ + 761.5359477124183, + 537.8137254901961 + ], + [ + 634.0849673202614, + 518.8594771241831 + ], + [ + 573.3006535947712, + 512.9771241830066 + ], + [ + 534.0849673202614, + 513.6307189542484 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "mead_index_cards", + "points": [ + [ + 447.156862745098, + 394.6764705882353 + ], + [ + 410.55555555555554, + 480.95098039215685 + ], + [ + 415.1307189542483, + 522.781045751634 + ], + [ + 422.9738562091503, + 522.781045751634 + ], + [ + 427.5490196078431, + 515.5915032679738 + ], + [ + 570.032679738562, + 512.3235294117648 + ], + [ + 732.1241830065359, + 528.0098039215686 + ], + [ + 733.4313725490196, + 492.71568627450984 + ], + [ + 724.9346405228757, + 407.0947712418301 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "kong_air_dog_squeakair_tennis_ball", + "points": [ + [ + 419.05228758169926, + 266.5718954248366 + ], + [ + 343.235294117647, + 303.1732026143791 + ], + [ + 511.2091503267974, + 318.859477124183 + ], + [ + 519.7058823529411, + 334.5457516339869 + ], + [ + 533.4313725490196, + 339.7745098039216 + ], + [ + 551.7320261437908, + 349.57843137254906 + ], + [ + 573.3006535947712, + 354.15359477124184 + ], + [ + 592.2549019607843, + 353.5 + ], + [ + 604.0196078431372, + 345.0032679738562 + ], + [ + 613.1699346405228, + 341.7352941176471 + ], + [ + 687.0261437908497, + 365.91830065359477 + ], + [ + 696.1764705882352, + 358.07516339869284 + ], + [ + 727.5490196078431, + 329.3169934640523 + ], + [ + 677.2222222222222, + 306.44117647058823 + ], + [ + 651.078431372549, + 273.76143790849676 + ], + [ + 632.1241830065359, + 272.4542483660131 + ], + [ + 612.516339869281, + 248.27124183006538 + ], + [ + 596.8300653594771, + 241.08169934640526 + ], + [ + 577.2222222222222, + 234.54575163398695 + ], + [ + 563.4967320261437, + 234.54575163398695 + ], + [ + 545.1960784313725, + 235.19934640522877 + ], + [ + 534.7385620915032, + 242.3888888888889 + ], + [ + 526.2418300653594, + 247.61764705882354 + ], + [ + 513.1699346405228, + 258.7287581699347 + ], + [ + 507.28758169934633, + 266.5718954248366 + ], + [ + 502.0588235294117, + 272.4542483660131 + ], + [ + 496.17647058823525, + 273.1078431372549 + ], + [ + 474.6078431372549, + 273.1078431372549 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "apc2016_obj3.json", + "imageData": "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", + "imageHeight": 907, + "imageWidth": 1210 +} \ No newline at end of file diff --git a/examples/tutorial/apc2016_obj3_json/img.png b/examples/tutorial/apc2016_obj3_json/img.png new file mode 100644 index 0000000..d3d5fed Binary files /dev/null and b/examples/tutorial/apc2016_obj3_json/img.png differ diff --git a/examples/tutorial/apc2016_obj3_json/label.png b/examples/tutorial/apc2016_obj3_json/label.png new file mode 100644 index 0000000..ed166bc Binary files /dev/null and b/examples/tutorial/apc2016_obj3_json/label.png differ diff --git a/examples/tutorial/apc2016_obj3_json/label_names.txt b/examples/tutorial/apc2016_obj3_json/label_names.txt new file mode 100644 index 0000000..4e4e0e1 --- /dev/null +++ b/examples/tutorial/apc2016_obj3_json/label_names.txt @@ -0,0 +1,5 @@ +_background_ +highland_6539_self_stick_notes +kong_air_dog_squeakair_tennis_ball +mead_index_cards +shelf diff --git a/examples/tutorial/apc2016_obj3_json/label_viz.png b/examples/tutorial/apc2016_obj3_json/label_viz.png new file mode 100644 index 0000000..4f88cda Binary files /dev/null and b/examples/tutorial/apc2016_obj3_json/label_viz.png differ diff --git a/examples/tutorial/load_label_png.py b/examples/tutorial/load_label_png.py new file mode 100644 index 0000000..f72c8ce --- /dev/null +++ b/examples/tutorial/load_label_png.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import os.path as osp + +import numpy as np +import PIL.Image + +here = osp.dirname(osp.abspath(__file__)) + + +def main(): + label_png = osp.join(here, "apc2016_obj3_json/label.png") + print("Loading:", label_png) + print() + + lbl = np.asarray(PIL.Image.open(label_png)) + labels = np.unique(lbl) + + label_names_txt = osp.join(here, "apc2016_obj3_json/label_names.txt") + label_names = [name.strip() for name in open(label_names_txt)] + print("# of labels:", len(labels)) + print("# of label_names:", len(label_names)) + if len(labels) != len(label_names): + print("Number of unique labels and label_names must be same.") + quit(1) + print() + + print("label: label_name") + for label, label_name in zip(labels, label_names): + print("%d: %s" % (label, label_name)) + + +if __name__ == "__main__": + main() diff --git a/examples/video_annotation/.readme/00000100.jpg b/examples/video_annotation/.readme/00000100.jpg new file mode 100644 index 0000000..5e210fc Binary files /dev/null and b/examples/video_annotation/.readme/00000100.jpg differ diff --git a/examples/video_annotation/.readme/00000101.jpg b/examples/video_annotation/.readme/00000101.jpg new file mode 100644 index 0000000..fa3e8a4 Binary files /dev/null and b/examples/video_annotation/.readme/00000101.jpg differ diff --git a/examples/video_annotation/.readme/data_annotated.gif b/examples/video_annotation/.readme/data_annotated.gif new file mode 100644 index 0000000..bf27091 Binary files /dev/null and b/examples/video_annotation/.readme/data_annotated.gif differ diff --git a/examples/video_annotation/README.md b/examples/video_annotation/README.md new file mode 100644 index 0000000..4649d0e --- /dev/null +++ b/examples/video_annotation/README.md @@ -0,0 +1,29 @@ +# Video Annotation Example + + +## Annotation + +```bash +labelme data_annotated --labels labels.txt --nodata --keep-prev --config '{shift_auto_shape_color: -2}' +``` + + + +*Fig 1. Video annotation example. A frame (left), The next frame (right).* + + + + +*Fig 2. Visualization of video semantic segmentation.* + + +## How to Convert a Video File to Images for Annotation? + +```bash +pip install video-cli + +video-toimg your_video.mp4 # this creates your_video/ directory +ls your_video/ + +labelme your_video/ +``` diff --git a/examples/video_annotation/data_annotated/00000100.jpg b/examples/video_annotation/data_annotated/00000100.jpg new file mode 100644 index 0000000..0969972 Binary files /dev/null and b/examples/video_annotation/data_annotated/00000100.jpg differ diff --git a/examples/video_annotation/data_annotated/00000100.json b/examples/video_annotation/data_annotated/00000100.json new file mode 100644 index 0000000..99ad2e7 --- /dev/null +++ b/examples/video_annotation/data_annotated/00000100.json @@ -0,0 +1,154 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "track", + "points": [ + [ + 634.0, + 203.25925925925924 + ], + [ + 604.0, + 274.25925925925924 + ], + [ + 603.0, + 339.25925925925924 + ], + [ + 622.0, + 362.25925925925924 + ], + [ + 639.0, + 362.25925925925924 + ], + [ + 649.0, + 353.25925925925924 + ], + [ + 682.0, + 382.25925925925924 + ], + [ + 733.0, + 389.25925925925924 + ], + [ + 748.0, + 363.25925925925924 + ], + [ + 827.0, + 358.25925925925924 + ], + [ + 829.0, + 249.25925925925924 + ], + [ + 800.0, + 193.25925925925924 + ], + [ + 775.0, + 184.25925925925924 + ], + [ + 740.0, + 198.25925925925924 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "track", + "points": [ + [ + 860.0, + 190.0 + ], + [ + 997.0, + 186.0 + ], + [ + 998.0, + 305.0 + ], + [ + 924.0, + 320.0 + ], + [ + 905.0, + 352.0 + ], + [ + 877.0, + 353.0 + ], + [ + 869.0, + 245.0 + ], + [ + 879.0, + 222.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 924.0, + 321.0 + ], + [ + 905.0, + 352.0 + ], + [ + 909.0, + 388.0 + ], + [ + 936.0, + 404.0 + ], + [ + 959.0, + 411.0 + ], + [ + 966.0, + 431.0 + ], + [ + 1000.0, + 432.0 + ], + [ + 1000.0, + 306.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "00000100.jpg", + "imageData": null, + "imageHeight": 563, + "imageWidth": 1000 +} \ No newline at end of file diff --git a/examples/video_annotation/data_annotated/00000101.jpg b/examples/video_annotation/data_annotated/00000101.jpg new file mode 100644 index 0000000..ea5f99f Binary files /dev/null and b/examples/video_annotation/data_annotated/00000101.jpg differ diff --git a/examples/video_annotation/data_annotated/00000101.json b/examples/video_annotation/data_annotated/00000101.json new file mode 100644 index 0000000..38c0fe9 --- /dev/null +++ b/examples/video_annotation/data_annotated/00000101.json @@ -0,0 +1,154 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "track", + "points": [ + [ + 614.7407407407408, + 203.2592592592593 + ], + [ + 584.7407407407408, + 274.2592592592593 + ], + [ + 583.7407407407408, + 339.2592592592593 + ], + [ + 602.7407407407408, + 362.2592592592593 + ], + [ + 619.7407407407408, + 362.2592592592593 + ], + [ + 629.7407407407408, + 353.2592592592593 + ], + [ + 662.7407407407408, + 382.2592592592593 + ], + [ + 713.7407407407408, + 389.2592592592593 + ], + [ + 728.7407407407408, + 363.2592592592593 + ], + [ + 827.7407407407408, + 357.2592592592593 + ], + [ + 825.7407407407408, + 248.2592592592593 + ], + [ + 801.7407407407408, + 199.2592592592593 + ], + [ + 757.7407407407408, + 193.2592592592593 + ], + [ + 720.7407407407408, + 198.2592592592593 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "track", + "points": [ + [ + 860.0, + 190.0 + ], + [ + 997.0, + 186.0 + ], + [ + 998.0, + 305.0 + ], + [ + 924.0, + 320.0 + ], + [ + 905.0, + 352.0 + ], + [ + 877.0, + 353.0 + ], + [ + 869.0, + 245.0 + ], + [ + 879.0, + 222.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 924.0, + 321.0 + ], + [ + 905.0, + 352.0 + ], + [ + 909.0, + 388.0 + ], + [ + 936.0, + 404.0 + ], + [ + 959.0, + 411.0 + ], + [ + 966.0, + 431.0 + ], + [ + 1000.0, + 432.0 + ], + [ + 1000.0, + 306.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "00000101.jpg", + "imageData": null, + "imageHeight": 563, + "imageWidth": 1000 +} \ No newline at end of file diff --git a/examples/video_annotation/data_annotated/00000102.jpg b/examples/video_annotation/data_annotated/00000102.jpg new file mode 100644 index 0000000..673d847 Binary files /dev/null and b/examples/video_annotation/data_annotated/00000102.jpg differ diff --git a/examples/video_annotation/data_annotated/00000102.json b/examples/video_annotation/data_annotated/00000102.json new file mode 100644 index 0000000..dd06371 --- /dev/null +++ b/examples/video_annotation/data_annotated/00000102.json @@ -0,0 +1,154 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "track", + "points": [ + [ + 591.5185185185185, + 202.51851851851853 + ], + [ + 561.5185185185185, + 273.51851851851853 + ], + [ + 560.5185185185185, + 338.51851851851853 + ], + [ + 579.5185185185185, + 361.51851851851853 + ], + [ + 596.5185185185185, + 361.51851851851853 + ], + [ + 606.5185185185185, + 352.51851851851853 + ], + [ + 639.5185185185185, + 381.51851851851853 + ], + [ + 690.5185185185185, + 388.51851851851853 + ], + [ + 705.5185185185185, + 362.51851851851853 + ], + [ + 825.5185185185185, + 356.51851851851853 + ], + [ + 821.5185185185185, + 241.51851851851853 + ], + [ + 800.5185185185185, + 197.51851851851853 + ], + [ + 734.5185185185185, + 192.51851851851853 + ], + [ + 697.5185185185185, + 197.51851851851853 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "track", + "points": [ + [ + 860.0, + 190.0 + ], + [ + 997.0, + 186.0 + ], + [ + 998.0, + 305.0 + ], + [ + 924.0, + 320.0 + ], + [ + 905.0, + 352.0 + ], + [ + 877.0, + 353.0 + ], + [ + 869.0, + 245.0 + ], + [ + 879.0, + 222.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 924.0, + 321.0 + ], + [ + 905.0, + 352.0 + ], + [ + 909.0, + 388.0 + ], + [ + 936.0, + 404.0 + ], + [ + 959.0, + 411.0 + ], + [ + 966.0, + 431.0 + ], + [ + 1000.0, + 432.0 + ], + [ + 1000.0, + 306.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "00000102.jpg", + "imageData": null, + "imageHeight": 563, + "imageWidth": 1000 +} \ No newline at end of file diff --git a/examples/video_annotation/data_annotated/00000103.jpg b/examples/video_annotation/data_annotated/00000103.jpg new file mode 100644 index 0000000..340b7cb Binary files /dev/null and b/examples/video_annotation/data_annotated/00000103.jpg differ diff --git a/examples/video_annotation/data_annotated/00000103.json b/examples/video_annotation/data_annotated/00000103.json new file mode 100644 index 0000000..438c11a --- /dev/null +++ b/examples/video_annotation/data_annotated/00000103.json @@ -0,0 +1,154 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "track", + "points": [ + [ + 573.0, + 202.55555555555554 + ], + [ + 543.0, + 273.55555555555554 + ], + [ + 542.0, + 338.55555555555554 + ], + [ + 561.0, + 361.55555555555554 + ], + [ + 578.0, + 361.55555555555554 + ], + [ + 588.0, + 352.55555555555554 + ], + [ + 621.0, + 381.55555555555554 + ], + [ + 672.0, + 388.55555555555554 + ], + [ + 687.0, + 362.55555555555554 + ], + [ + 829.0, + 349.55555555555554 + ], + [ + 821.0, + 231.55555555555554 + ], + [ + 801.0, + 194.55555555555554 + ], + [ + 716.0, + 192.55555555555554 + ], + [ + 679.0, + 197.55555555555554 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "track", + "points": [ + [ + 860.0, + 190.0 + ], + [ + 997.0, + 186.0 + ], + [ + 998.0, + 305.0 + ], + [ + 924.0, + 320.0 + ], + [ + 905.0, + 352.0 + ], + [ + 877.0, + 353.0 + ], + [ + 869.0, + 245.0 + ], + [ + 879.0, + 222.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 924.0, + 321.0 + ], + [ + 905.0, + 352.0 + ], + [ + 909.0, + 388.0 + ], + [ + 936.0, + 404.0 + ], + [ + 959.0, + 411.0 + ], + [ + 966.0, + 431.0 + ], + [ + 1000.0, + 432.0 + ], + [ + 1000.0, + 306.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "00000103.jpg", + "imageData": null, + "imageHeight": 563, + "imageWidth": 1000 +} \ No newline at end of file diff --git a/examples/video_annotation/data_annotated/00000104.jpg b/examples/video_annotation/data_annotated/00000104.jpg new file mode 100644 index 0000000..b03b99b Binary files /dev/null and b/examples/video_annotation/data_annotated/00000104.jpg differ diff --git a/examples/video_annotation/data_annotated/00000104.json b/examples/video_annotation/data_annotated/00000104.json new file mode 100644 index 0000000..fd80c37 --- /dev/null +++ b/examples/video_annotation/data_annotated/00000104.json @@ -0,0 +1,154 @@ +{ + "version": "4.0.0", + "flags": {}, + "shapes": [ + { + "label": "track", + "points": [ + [ + 555.2592592592594, + 200.25925925925924 + ], + [ + 527.2592592592594, + 276.25925925925924 + ], + [ + 523.2592592592594, + 341.25925925925924 + ], + [ + 527.2592592592594, + 360.25925925925924 + ], + [ + 562.2592592592594, + 364.25925925925924 + ], + [ + 572.2592592592594, + 355.25925925925924 + ], + [ + 605.2592592592594, + 384.25925925925924 + ], + [ + 656.2592592592594, + 391.25925925925924 + ], + [ + 671.2592592592594, + 365.25925925925924 + ], + [ + 824.2592592592594, + 353.25925925925924 + ], + [ + 825.2592592592594, + 237.25925925925924 + ], + [ + 800.2592592592594, + 201.25925925925924 + ], + [ + 700.2592592592594, + 195.25925925925924 + ], + [ + 663.2592592592594, + 200.25925925925924 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "track", + "points": [ + [ + 860.0, + 190.0 + ], + [ + 997.0, + 186.0 + ], + [ + 998.0, + 305.0 + ], + [ + 924.0, + 320.0 + ], + [ + 905.0, + 352.0 + ], + [ + 874.0, + 354.0 + ], + [ + 869.0, + 245.0 + ], + [ + 879.0, + 222.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + }, + { + "label": "car", + "points": [ + [ + 924.0, + 321.0 + ], + [ + 905.0, + 352.0 + ], + [ + 909.0, + 388.0 + ], + [ + 936.0, + 404.0 + ], + [ + 959.0, + 411.0 + ], + [ + 966.0, + 431.0 + ], + [ + 1000.0, + 432.0 + ], + [ + 1000.0, + 306.0 + ] + ], + "group_id": null, + "shape_type": "polygon", + "flags": {} + } + ], + "imagePath": "00000104.jpg", + "imageData": null, + "imageHeight": 563, + "imageWidth": 1000 +} \ No newline at end of file diff --git a/examples/video_annotation/data_dataset_voc/JPEGImages/00000100.jpg b/examples/video_annotation/data_dataset_voc/JPEGImages/00000100.jpg new file mode 100644 index 0000000..a998170 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/JPEGImages/00000100.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/JPEGImages/00000101.jpg b/examples/video_annotation/data_dataset_voc/JPEGImages/00000101.jpg new file mode 100644 index 0000000..149068a Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/JPEGImages/00000101.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/JPEGImages/00000102.jpg b/examples/video_annotation/data_dataset_voc/JPEGImages/00000102.jpg new file mode 100644 index 0000000..bfab87a Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/JPEGImages/00000102.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/JPEGImages/00000103.jpg b/examples/video_annotation/data_dataset_voc/JPEGImages/00000103.jpg new file mode 100644 index 0000000..ff1155b Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/JPEGImages/00000103.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/JPEGImages/00000104.jpg b/examples/video_annotation/data_dataset_voc/JPEGImages/00000104.jpg new file mode 100644 index 0000000..c08aa82 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/JPEGImages/00000104.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClass/00000100.npy b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000100.npy new file mode 100644 index 0000000..f601427 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000100.npy differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClass/00000101.npy b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000101.npy new file mode 100644 index 0000000..5e18e28 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000101.npy differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClass/00000102.npy b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000102.npy new file mode 100644 index 0000000..8571911 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000102.npy differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClass/00000103.npy b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000103.npy new file mode 100644 index 0000000..63c5d2d Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000103.npy differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClass/00000104.npy b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000104.npy new file mode 100644 index 0000000..a49b677 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClass/00000104.npy differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000100.png b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000100.png new file mode 100644 index 0000000..5eb8fc1 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000100.png differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000101.png b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000101.png new file mode 100644 index 0000000..41388f7 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000101.png differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000102.png b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000102.png new file mode 100644 index 0000000..a86d455 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000102.png differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000103.png b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000103.png new file mode 100644 index 0000000..d27b6eb Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000103.png differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000104.png b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000104.png new file mode 100644 index 0000000..77a5a22 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassPNG/00000104.png differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000100.jpg b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000100.jpg new file mode 100644 index 0000000..1f07aed Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000100.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000101.jpg b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000101.jpg new file mode 100644 index 0000000..24378d8 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000101.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000102.jpg b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000102.jpg new file mode 100644 index 0000000..6931211 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000102.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000103.jpg b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000103.jpg new file mode 100644 index 0000000..7bfb741 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000103.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000104.jpg b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000104.jpg new file mode 100644 index 0000000..5fc70c2 Binary files /dev/null and b/examples/video_annotation/data_dataset_voc/SegmentationClassVisualization/00000104.jpg differ diff --git a/examples/video_annotation/data_dataset_voc/class_names.txt b/examples/video_annotation/data_dataset_voc/class_names.txt new file mode 100644 index 0000000..3342496 --- /dev/null +++ b/examples/video_annotation/data_dataset_voc/class_names.txt @@ -0,0 +1,3 @@ +_background_ +car +track \ No newline at end of file diff --git a/examples/video_annotation/labelme2voc.py b/examples/video_annotation/labelme2voc.py new file mode 120000 index 0000000..03bbbb8 --- /dev/null +++ b/examples/video_annotation/labelme2voc.py @@ -0,0 +1 @@ +../semantic_segmentation/labelme2voc.py \ No newline at end of file diff --git a/examples/video_annotation/labels.txt b/examples/video_annotation/labels.txt new file mode 100644 index 0000000..077705b --- /dev/null +++ b/examples/video_annotation/labels.txt @@ -0,0 +1,4 @@ +__ignore__ +_background_ +car +track diff --git a/how_to_run.txt b/how_to_run.txt new file mode 100644 index 0000000..392b2ac --- /dev/null +++ b/how_to_run.txt @@ -0,0 +1,13 @@ +--- Installation +conda create --name=trackGUI python=3.8 +conda activate trackGUI +pip install -e . +--- Run the program and input this into the terminal. This program can only run in this "labelme" conda environment. +conda activate trackGUI +labelme + +Note: +All frames must have labeled boxes ++ Track from scratch: track from the first frame to the end frame with automatic ID assignment ++ Track from Current Frame w/ Annotation: track from the current (being opened) frame with the modified ID or manual ID assignment ++ Track from Current Frame w/0 Annotation: track from the current (being opened) frame with automatic ID assignment diff --git a/labelme.desktop b/labelme.desktop new file mode 100644 index 0000000..3e9db68 --- /dev/null +++ b/labelme.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Labelme +Comment=Image Polygonal Annotation with Python +Exec=labelme +Icon=labelme +Terminal=false +Type=Application +Categories=Graphics;RasterGraphics; diff --git a/labelme.spec b/labelme.spec new file mode 100644 index 0000000..389e34d --- /dev/null +++ b/labelme.spec @@ -0,0 +1,45 @@ +# -*- mode: python -*- +# vim: ft=python + +import sys + + +sys.setrecursionlimit(5000) # required on Windows + + +a = Analysis( + ['labelme/__main__.py'], + pathex=['labelme'], + binaries=[], + datas=[ + ('labelme/config/default_config.yaml', 'labelme/config'), + ('labelme/icons/*', 'labelme/icons'), + ('labelme/translate/*.qm', 'translate'), + ], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], +) +pyz = PYZ(a.pure, a.zipped_data) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='labelme', + debug=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=False, + icon='labelme/icons/icon.ico', +) +app = BUNDLE( + exe, + name='Labelme.app', + icon='labelme/icons/icon.icns', + bundle_identifier=None, + info_plist={'NSHighResolutionCapable': 'True'}, +) diff --git a/labelme/__init__.py b/labelme/__init__.py new file mode 100644 index 0000000..1cbd4d8 --- /dev/null +++ b/labelme/__init__.py @@ -0,0 +1,28 @@ +# flake8: noqa + +import logging +import sys + +from qtpy import QT_VERSION + + +__appname__ = "TrackMe" + +# Semantic Versioning 2.0.0: https://semver.org/ +# 1. MAJOR version when you make incompatible API changes; +# 2. MINOR version when you add functionality in a backwards-compatible manner; +# 3. PATCH version when you make backwards-compatible bug fixes. +# e.g., 1.0.0a0, 1.0.0a1, 1.0.0b0, 1.0.0rc0, 1.0.0, 1.0.0.post0 +__version__ = "5.4.1" + +QT4 = QT_VERSION[0] == "4" +QT5 = QT_VERSION[0] == "5" +del QT_VERSION + +PY2 = sys.version[0] == "2" +PY3 = sys.version[0] == "3" +del sys + +from labelme.label_file import LabelFile +from labelme import testing +from labelme import utils diff --git a/labelme/__main__.py b/labelme/__main__.py new file mode 100644 index 0000000..98fe1ac --- /dev/null +++ b/labelme/__main__.py @@ -0,0 +1,187 @@ +import argparse +import codecs +import logging +import os +import os.path as osp +import sys + +import yaml +from qtpy import QtCore +from qtpy import QtWidgets + +from labelme import __appname__ +from labelme import __version__ +from labelme.app import MainWindow +from labelme.config import get_config +from labelme.logger import logger +from labelme.utils import newIcon + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--version", "-V", action="store_true", help="show version") + parser.add_argument("--reset-config", action="store_true", help="reset qt config") + parser.add_argument( + "--logger-level", + default="debug", + choices=["debug", "info", "warning", "fatal", "error"], + help="logger level", + ) + parser.add_argument("filename", nargs="?", help="image or label filename") + parser.add_argument( + "--output", + "-O", + "-o", + help="output file or directory (if it ends with .json it is " + "recognized as file, else as directory)", + ) + default_config_file = os.path.join(os.path.expanduser("~"), ".labelmerc") + parser.add_argument( + "--config", + dest="config", + help="config file or yaml-format string (default: {})".format( + default_config_file + ), + default=default_config_file, + ) + # config for the gui + parser.add_argument( + "--nodata", + dest="store_data", + action="store_false", + help="stop storing image data to JSON file", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--autosave", + dest="auto_save", + action="store_true", + help="auto save", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--nosortlabels", + dest="sort_labels", + action="store_false", + help="stop sorting labels", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--flags", + help="comma separated list of flags OR file containing flags", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--labelflags", + dest="label_flags", + help=r"yaml string of label specific flags OR file containing json " + r"string of label specific flags (ex. {person-\d+: [male, tall], " + r"dog-\d+: [black, brown, white], .*: [occluded]})", # NOQA + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--labels", + help="comma separated list of labels OR file containing labels", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--validatelabel", + dest="validate_label", + choices=["exact"], + help="label validation types", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--keep-prev", + action="store_true", + help="keep annotation of previous frame", + default=argparse.SUPPRESS, + ) + parser.add_argument( + "--epsilon", + type=float, + help="epsilon to find nearest vertex on canvas", + default=argparse.SUPPRESS, + ) + args = parser.parse_args() + + if args.version: + print("{0} {1}".format(__appname__, __version__)) + sys.exit(0) + + logger.setLevel(getattr(logging, args.logger_level.upper())) + + if hasattr(args, "flags"): + if os.path.isfile(args.flags): + with codecs.open(args.flags, "r", encoding="utf-8") as f: + args.flags = [line.strip() for line in f if line.strip()] + else: + args.flags = [line for line in args.flags.split(",") if line] + + if hasattr(args, "labels"): + if os.path.isfile(args.labels): + with codecs.open(args.labels, "r", encoding="utf-8") as f: + args.labels = [line.strip() for line in f if line.strip()] + else: + args.labels = [line for line in args.labels.split(",") if line] + + if hasattr(args, "label_flags"): + if os.path.isfile(args.label_flags): + with codecs.open(args.label_flags, "r", encoding="utf-8") as f: + args.label_flags = yaml.safe_load(f) + else: + args.label_flags = yaml.safe_load(args.label_flags) + + config_from_args = args.__dict__ + config_from_args.pop("version") + reset_config = config_from_args.pop("reset_config") + filename = config_from_args.pop("filename") + output = config_from_args.pop("output") + config_file_or_yaml = config_from_args.pop("config") + config = get_config(config_file_or_yaml, config_from_args) + + if not config["labels"] and config["validate_label"]: + logger.error( + "--labels must be specified with --validatelabel or " + "validate_label: true in the config file " + "(ex. ~/.labelmerc)." + ) + sys.exit(1) + + output_file = None + output_dir = None + if output is not None: + if output.endswith(".json"): + output_file = output + else: + output_dir = output + + translator = QtCore.QTranslator() + translator.load( + QtCore.QLocale.system().name(), + osp.dirname(osp.abspath(__file__)) + "/translate", + ) + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName(__appname__) + app.setWindowIcon(newIcon("icon")) + app.installTranslator(translator) + win = MainWindow( + config=config, + filename=filename, + output_file=output_file, + output_dir=output_dir, + ) + + if reset_config: + logger.info("Resetting Qt config: %s" % win.settings.fileName()) + win.settings.clear() + sys.exit(0) + + win.show() + win.raise_() + sys.exit(app.exec_()) + + +# this main block is required to generate executable by pyinstaller +if __name__ == "__main__": + main() diff --git a/labelme/ai/__init__.py b/labelme/ai/__init__.py new file mode 100644 index 0000000..1dad867 --- /dev/null +++ b/labelme/ai/__init__.py @@ -0,0 +1,93 @@ +import gdown + +from .efficient_sam import EfficientSam +from .segment_anything_model import SegmentAnythingModel + + +class SegmentAnythingModelVitB(SegmentAnythingModel): + name = "SegmentAnything (speed)" + + def __init__(self): + super().__init__( + encoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_b_01ec64.quantized.encoder.onnx", # NOQA + md5="80fd8d0ab6c6ae8cb7b3bd5f368a752c", + ), + decoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_b_01ec64.quantized.decoder.onnx", # NOQA + md5="4253558be238c15fc265a7a876aaec82", + ), + ) + + +class SegmentAnythingModelVitL(SegmentAnythingModel): + name = "SegmentAnything (balanced)" + + def __init__(self): + super().__init__( + encoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_l_0b3195.quantized.encoder.onnx", # NOQA + md5="080004dc9992724d360a49399d1ee24b", + ), + decoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_l_0b3195.quantized.decoder.onnx", # NOQA + md5="851b7faac91e8e23940ee1294231d5c7", + ), + ) + + +class SegmentAnythingModelVitH(SegmentAnythingModel): + name = "SegmentAnything (accuracy)" + + def __init__(self): + super().__init__( + encoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_h_4b8939.quantized.encoder.onnx", # NOQA + md5="958b5710d25b198d765fb6b94798f49e", + ), + decoder_path=gdown.cached_download( + url="https://github.com/wkentaro/labelme/releases/download/sam-20230416/sam_vit_h_4b8939.quantized.decoder.onnx", # NOQA + md5="a997a408347aa081b17a3ffff9f42a80", + ), + ) + + +class EfficientSamVitT(EfficientSam): + name = "EfficientSam (speed)" + + def __init__(self): + super().__init__( + encoder_path=gdown.cached_download( + url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vitt_encoder.onnx", # NOQA + md5="2d4a1303ff0e19fe4a8b8ede69c2f5c7", + ), + decoder_path=gdown.cached_download( + url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vitt_decoder.onnx", # NOQA + md5="be3575ca4ed9b35821ac30991ab01843", + ), + ) + + +class EfficientSamVitS(EfficientSam): + name = "EfficientSam (accuracy)" + + def __init__(self): + super().__init__( + encoder_path=gdown.cached_download( + url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vits_encoder.onnx", # NOQA + md5="7d97d23e8e0847d4475ca7c9f80da96d", + ), + decoder_path=gdown.cached_download( + url="https://github.com/labelmeai/efficient-sam/releases/download/onnx-models-20231225/efficient_sam_vits_decoder.onnx", # NOQA + md5="d9372f4a7bbb1a01d236b0508300b994", + ), + ) + + +MODELS = [ + SegmentAnythingModelVitB, + SegmentAnythingModelVitL, + SegmentAnythingModelVitH, + EfficientSamVitT, + EfficientSamVitS, +] diff --git a/labelme/ai/_utils.py b/labelme/ai/_utils.py new file mode 100644 index 0000000..6806a5b --- /dev/null +++ b/labelme/ai/_utils.py @@ -0,0 +1,38 @@ +import imgviz +import numpy as np +import skimage + +from labelme.logger import logger + + +def _get_contour_length(contour): + contour_start = contour + contour_end = np.r_[contour[1:], contour[0:1]] + return np.linalg.norm(contour_end - contour_start, axis=1).sum() + + +def compute_polygon_from_mask(mask): + contours = skimage.measure.find_contours(np.pad(mask, pad_width=1)) + if len(contours) == 0: + logger.warning("No contour found, so returning empty polygon.") + return np.empty((0, 2), dtype=np.float32) + + contour = max(contours, key=_get_contour_length) + POLYGON_APPROX_TOLERANCE = 0.004 + polygon = skimage.measure.approximate_polygon( + coords=contour, + tolerance=np.ptp(contour, axis=0).max() * POLYGON_APPROX_TOLERANCE, + ) + polygon = np.clip(polygon, (0, 0), (mask.shape[0] - 1, mask.shape[1] - 1)) + polygon = polygon[:-1] # drop last point that is duplicate of first point + + if 0: + import PIL.Image + + image_pil = PIL.Image.fromarray(imgviz.gray2rgb(imgviz.bool2ubyte(mask))) + imgviz.draw.line_(image_pil, yx=polygon, fill=(0, 255, 0)) + for point in polygon: + imgviz.draw.circle_(image_pil, center=point, diameter=10, fill=(0, 255, 0)) + imgviz.io.imsave("contour.jpg", np.asarray(image_pil)) + + return polygon[:, ::-1] # yx -> xy diff --git a/labelme/ai/efficient_sam.py b/labelme/ai/efficient_sam.py new file mode 100644 index 0000000..656c43a --- /dev/null +++ b/labelme/ai/efficient_sam.py @@ -0,0 +1,100 @@ +import collections +import threading + +import imgviz +import numpy as np +import onnxruntime +import skimage + +from ..logger import logger +from . import _utils + + +class EfficientSam: + def __init__(self, encoder_path, decoder_path): + self._encoder_session = onnxruntime.InferenceSession(encoder_path) + self._decoder_session = onnxruntime.InferenceSession(decoder_path) + + self._lock = threading.Lock() + self._image_embedding_cache = collections.OrderedDict() + + self._thread = None + + def set_image(self, image: np.ndarray): + with self._lock: + self._image = image + self._image_embedding = self._image_embedding_cache.get( + self._image.tobytes() + ) + + if self._image_embedding is None: + self._thread = threading.Thread( + target=self._compute_and_cache_image_embedding + ) + self._thread.start() + + def _compute_and_cache_image_embedding(self): + with self._lock: + logger.debug("Computing image embedding...") + image = imgviz.rgba2rgb(self._image) + batched_images = image.transpose(2, 0, 1)[None].astype(np.float32) / 255.0 + (self._image_embedding,) = self._encoder_session.run( + output_names=None, + input_feed={"batched_images": batched_images}, + ) + if len(self._image_embedding_cache) > 10: + self._image_embedding_cache.popitem(last=False) + self._image_embedding_cache[self._image.tobytes()] = self._image_embedding + logger.debug("Done computing image embedding.") + + def _get_image_embedding(self): + if self._thread is not None: + self._thread.join() + self._thread = None + with self._lock: + return self._image_embedding + + def predict_mask_from_points(self, points, point_labels): + return _compute_mask_from_points( + decoder_session=self._decoder_session, + image=self._image, + image_embedding=self._get_image_embedding(), + points=points, + point_labels=point_labels, + ) + + def predict_polygon_from_points(self, points, point_labels): + mask = self.predict_mask_from_points(points=points, point_labels=point_labels) + return _utils.compute_polygon_from_mask(mask=mask) + + +def _compute_mask_from_points( + decoder_session, image, image_embedding, points, point_labels +): + input_point = np.array(points, dtype=np.float32) + input_label = np.array(point_labels, dtype=np.float32) + + # batch_size, num_queries, num_points, 2 + batched_point_coords = input_point[None, None, :, :] + # batch_size, num_queries, num_points + batched_point_labels = input_label[None, None, :] + + decoder_inputs = { + "image_embeddings": image_embedding, + "batched_point_coords": batched_point_coords, + "batched_point_labels": batched_point_labels, + "orig_im_size": np.array(image.shape[:2], dtype=np.int64), + } + + masks, _, _ = decoder_session.run(None, decoder_inputs) + mask = masks[0, 0, 0, :, :] # (1, 1, 3, H, W) -> (H, W) + mask = mask > 0.0 + + MIN_SIZE_RATIO = 0.05 + skimage.morphology.remove_small_objects( + mask, min_size=mask.sum() * MIN_SIZE_RATIO, out=mask + ) + + if 0: + imgviz.io.imsave("mask.jpg", imgviz.label2rgb(mask, imgviz.rgb2gray(image))) + return mask diff --git a/labelme/ai/segment_anything_model.py b/labelme/ai/segment_anything_model.py new file mode 100644 index 0000000..84da17d --- /dev/null +++ b/labelme/ai/segment_anything_model.py @@ -0,0 +1,166 @@ +import collections +import threading + +import imgviz +import numpy as np +import onnxruntime +import skimage + +from ..logger import logger +from . import _utils + + +class SegmentAnythingModel: + def __init__(self, encoder_path, decoder_path): + self._image_size = 1024 + + self._encoder_session = onnxruntime.InferenceSession(encoder_path) + self._decoder_session = onnxruntime.InferenceSession(decoder_path) + + self._lock = threading.Lock() + self._image_embedding_cache = collections.OrderedDict() + + self._thread = None + + def set_image(self, image: np.ndarray): + with self._lock: + self._image = image + self._image_embedding = self._image_embedding_cache.get( + self._image.tobytes() + ) + + if self._image_embedding is None: + self._thread = threading.Thread( + target=self._compute_and_cache_image_embedding + ) + self._thread.start() + + def _compute_and_cache_image_embedding(self): + with self._lock: + logger.debug("Computing image embedding...") + self._image_embedding = _compute_image_embedding( + image_size=self._image_size, + encoder_session=self._encoder_session, + image=self._image, + ) + if len(self._image_embedding_cache) > 10: + self._image_embedding_cache.popitem(last=False) + self._image_embedding_cache[self._image.tobytes()] = self._image_embedding + logger.debug("Done computing image embedding.") + + def _get_image_embedding(self): + if self._thread is not None: + self._thread.join() + self._thread = None + with self._lock: + return self._image_embedding + + def predict_mask_from_points(self, points, point_labels): + return _compute_mask_from_points( + image_size=self._image_size, + decoder_session=self._decoder_session, + image=self._image, + image_embedding=self._get_image_embedding(), + points=points, + point_labels=point_labels, + ) + + def predict_polygon_from_points(self, points, point_labels): + mask = self.predict_mask_from_points(points=points, point_labels=point_labels) + return _utils.compute_polygon_from_mask(mask=mask) + + +def _compute_scale_to_resize_image(image_size, image): + height, width = image.shape[:2] + if width > height: + scale = image_size / width + new_height = int(round(height * scale)) + new_width = image_size + else: + scale = image_size / height + new_height = image_size + new_width = int(round(width * scale)) + return scale, new_height, new_width + + +def _resize_image(image_size, image): + scale, new_height, new_width = _compute_scale_to_resize_image( + image_size=image_size, image=image + ) + scaled_image = imgviz.resize( + image, + height=new_height, + width=new_width, + backend="pillow", + ).astype(np.float32) + return scale, scaled_image + + +def _compute_image_embedding(image_size, encoder_session, image): + image = imgviz.asrgb(image) + + scale, x = _resize_image(image_size, image) + x = (x - np.array([123.675, 116.28, 103.53], dtype=np.float32)) / np.array( + [58.395, 57.12, 57.375], dtype=np.float32 + ) + x = np.pad( + x, + ( + (0, image_size - x.shape[0]), + (0, image_size - x.shape[1]), + (0, 0), + ), + ) + x = x.transpose(2, 0, 1)[None, :, :, :] + + output = encoder_session.run(output_names=None, input_feed={"x": x}) + image_embedding = output[0] + + return image_embedding + + +def _compute_mask_from_points( + image_size, decoder_session, image, image_embedding, points, point_labels +): + input_point = np.array(points, dtype=np.float32) + input_label = np.array(point_labels, dtype=np.int32) + + onnx_coord = np.concatenate([input_point, np.array([[0.0, 0.0]])], axis=0)[ + None, :, : + ] + onnx_label = np.concatenate([input_label, np.array([-1])], axis=0)[None, :].astype( + np.float32 + ) + + scale, new_height, new_width = _compute_scale_to_resize_image( + image_size=image_size, image=image + ) + onnx_coord = ( + onnx_coord.astype(float) + * (new_width / image.shape[1], new_height / image.shape[0]) + ).astype(np.float32) + + onnx_mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32) + onnx_has_mask_input = np.array([-1], dtype=np.float32) + + decoder_inputs = { + "image_embeddings": image_embedding, + "point_coords": onnx_coord, + "point_labels": onnx_label, + "mask_input": onnx_mask_input, + "has_mask_input": onnx_has_mask_input, + "orig_im_size": np.array(image.shape[:2], dtype=np.float32), + } + + masks, _, _ = decoder_session.run(None, decoder_inputs) + mask = masks[0, 0] # (1, 1, H, W) -> (H, W) + mask = mask > 0.0 + + MIN_SIZE_RATIO = 0.05 + skimage.morphology.remove_small_objects( + mask, min_size=mask.sum() * MIN_SIZE_RATIO, out=mask + ) + + if 0: + imgviz.io.imsave("mask.jpg", imgviz.label2rgb(mask, imgviz.rgb2gray(image))) + return mask diff --git a/labelme/app.py b/labelme/app.py new file mode 100644 index 0000000..a3533f1 --- /dev/null +++ b/labelme/app.py @@ -0,0 +1,2836 @@ +# -*- coding: utf-8 -*- + +import functools +import html +import math +import os +import os.path as osp +import re +import webbrowser + +import imgviz +import natsort +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt + +from labelme import PY2 +from labelme import __appname__ +from labelme.ai import MODELS +from labelme.config import get_config +from labelme.label_file import LabelFile +from labelme.label_file import LabelFileError +from labelme.logger import logger +from labelme.shape import Shape +from labelme.widgets import BrightnessContrastDialog +from labelme.widgets import Canvas +from labelme.widgets import FileDialogPreview +from labelme.widgets import LabelDialog +from labelme.widgets import IDDialog +from labelme.widgets import LabelListWidget +from labelme.widgets import LabelListWidgetItem +from labelme.widgets import IDListWidget +from labelme.widgets import IDListWidgetItem +from labelme.widgets import NavigationWidget +from labelme.widgets import TrackDialog +from labelme.widgets import InterpolationDialog +from labelme.widgets import IterpolationRefineWidget +from labelme.widgets import InterpolationRefineInfo_Dialog +from labelme.widgets import DeletionDialog +from labelme.widgets import ToolBar +from labelme.widgets import UniqueLabelQListWidget +from labelme.widgets import ZoomWidget + +from . import utils + +import numpy as np +import json +from labelme.track_algo import SORT_main +from labelme.track_algo import KalmanBoxTracker +from scipy.optimize import linear_sum_assignment + +# FIXME +# - [medium] Set max zoom value to something big enough for FitWidth/Window + +# TODO(unknown): +# - Zoom is too "steppy". + + +LABEL_COLORMAP = imgviz.label_colormap() + + +class MainWindow(QtWidgets.QMainWindow): + FIT_WINDOW, FIT_WIDTH, MANUAL_ZOOM = 0, 1, 2 + + def __init__( + self, + config=None, + filename=None, + output=None, + output_file=None, + output_dir=None, + ): + if output is not None: + logger.warning("argument output is deprecated, use output_file instead") + if output_file is None: + output_file = output + + # see labelme/config/default_config.yaml for valid configuration + if config is None: + config = get_config() + self._config = config + + self._config["auto_save"] = True + self._config["store_data"] = False + + # set default shape colors + Shape.line_color = QtGui.QColor(*self._config["shape"]["line_color"]) + Shape.fill_color = QtGui.QColor(*self._config["shape"]["fill_color"]) + Shape.select_line_color = QtGui.QColor( + *self._config["shape"]["select_line_color"] + ) + Shape.select_fill_color = QtGui.QColor( + *self._config["shape"]["select_fill_color"] + ) + Shape.vertex_fill_color = QtGui.QColor( + *self._config["shape"]["vertex_fill_color"] + ) + Shape.hvertex_fill_color = QtGui.QColor( + *self._config["shape"]["hvertex_fill_color"] + ) + + # Set point size from config file + Shape.point_size = self._config["shape"]["point_size"] + + super(MainWindow, self).__init__() + self.setWindowTitle(__appname__) + + # Whether we need to save or not. + self.dirty = False + + self._noSelectionSlot = False + + self._copied_shapes = None + + # Main widgets and related state. + self.labelDialog = LabelDialog( + parent=self, + labels=self._config["labels"], + sort_labels=self._config["sort_labels"], + show_text_field=self._config["show_label_text_field"], + completion=self._config["label_completion"], + fit_to_content=self._config["fit_to_content"], + flags=self._config["label_flags"], + ) + + self.IDDialog = IDDialog( + parent=self, + ids=self._config["labels"], + sort_ids=self._config["sort_labels"], + show_text_field=self._config["show_label_text_field"], + completion=self._config["label_completion"], + fit_to_content=self._config["fit_to_content"] + ) + + self.labelList = LabelListWidget() + self.lastOpenDir = None + + self.flag_dock = self.flag_widget = None + self.flag_dock = QtWidgets.QDockWidget(self.tr("Flags"), self) + self.flag_dock.setObjectName("Flags") + self.flag_widget = QtWidgets.QListWidget() + if config["flags"]: + self.loadFlags({k: False for k in config["flags"]}) + self.flag_dock.setWidget(self.flag_widget) + self.flag_widget.itemChanged.connect(self.setDirty) + + self.mode = "None" + self.list_length = "" + self.start_INP0 = 0 + self.end_INP0 = 0 + self.interval_INPO = 0 + self.ID_INPO = "" + self.label_INPO = "" + self.INTERPOLATION_list = [] + self.INTERPOLATION_filename = None + self.navigation_list = NavigationWidget() + self.navigation_list.button1.clicked.connect(self.OKAY) + self.navigation_list.button2.clicked.connect(self.openNextImg) + self.navigation_list.button3.clicked.connect(self.openPrevImg) + self.navigation_dock = QtWidgets.QDockWidget(self.tr("Navigation"), self) + self.navigation_dock.setObjectName("Navigation") + self.navigation_dock.setWidget(self.navigation_list) + + self.interpolationrefine_list = IterpolationRefineWidget() + self.interpolationrefine_list.button.clicked.connect(self.editIR_info) + self.interpolationrefine_dock = QtWidgets.QDockWidget(self.tr("Interpolation Refinement"), self) + self.interpolationrefine_dock.setObjectName("Interpolation Refinement") + self.interpolationrefine_dock.setWidget(self.interpolationrefine_list) + self.ir_name = "None" + self.ir_id = "None" + self.ir_old_shapes = [] + self.ir_old_shape = "None" + self.ir_mod_shape = "None" + self.ir_activated = False + # self.interpolationrefine_list.checkBox.isChecked() + + self.labelList.itemSelectionChanged.connect(self.labelSelectionChanged) + self.labelList.itemDoubleClicked.connect(self.editLabel) + self.labelList.itemChanged.connect(self.labelItemChanged) + self.labelList.itemDropped.connect(self.labelOrderChanged) + self.shape_dock = QtWidgets.QDockWidget(self.tr("Polygon Labels"), self) + self.shape_dock.setObjectName("Labels") + self.shape_dock.setWidget(self.labelList) + + self.IDList = IDListWidget() + self.IDList.itemSelectionChanged.connect(self.IDSelectionChanged) + self.IDList.itemDoubleClicked.connect(self.editID) + self.IDList.itemChanged.connect(self.IDItemChanged) + self.IDList.itemDropped.connect(self.IDOrderChanged) + self.shape_dock = QtWidgets.QDockWidget(self.tr("Polygon IDs"), self) + self.shape_dock.setObjectName("IDs") + self.shape_dock.setWidget(self.IDList) + + self.uniqLabelList = UniqueLabelQListWidget() + self.uniqLabelList.setToolTip( + self.tr( + "Select label to start annotating for it. " "Press 'Esc' to deselect." + ) + ) + if self._config["labels"]: + for label in self._config["labels"]: + item = self.uniqLabelList.createItemFromLabel(label) + self.uniqLabelList.addItem(item) + rgb = self._get_rgb_by_label(label) + self.uniqLabelList.setItemLabel(item, label, rgb) + self.label_dock = QtWidgets.QDockWidget(self.tr("Label List"), self) + self.label_dock.setObjectName("Label List") + self.label_dock.setWidget(self.uniqLabelList) + + self.fileSearch = QtWidgets.QLineEdit() + self.fileSearch.setPlaceholderText(self.tr("Search Filename")) + self.fileSearch.textChanged.connect(self.fileSearchChanged) + self.fileListWidget = QtWidgets.QListWidget() + self.fileListWidget.itemSelectionChanged.connect(self.fileSelectionChanged) + fileListLayout = QtWidgets.QVBoxLayout() + fileListLayout.setContentsMargins(0, 0, 0, 0) + fileListLayout.setSpacing(0) + fileListLayout.addWidget(self.fileSearch) + fileListLayout.addWidget(self.fileListWidget) + self.file_dock = QtWidgets.QDockWidget(self.tr("File List"), self) + self.file_dock.setObjectName("Files") + fileListWidget = QtWidgets.QWidget() + fileListWidget.setLayout(fileListLayout) + self.file_dock.setWidget(fileListWidget) + + self.zoomWidget = ZoomWidget() + self.setAcceptDrops(True) + + self.canvas = self.labelList.canvas = Canvas( + epsilon=self._config["epsilon"], + double_click=self._config["canvas"]["double_click"], + num_backups=self._config["canvas"]["num_backups"], + crosshair=self._config["canvas"]["crosshair"], + ) + self.canvas.zoomRequest.connect(self.zoomRequest) + + scrollArea = QtWidgets.QScrollArea() + scrollArea.setWidget(self.canvas) + scrollArea.setWidgetResizable(True) + self.scrollBars = { + Qt.Vertical: scrollArea.verticalScrollBar(), + Qt.Horizontal: scrollArea.horizontalScrollBar(), + } + self.canvas.scrollRequest.connect(self.scrollRequest) + + self.canvas.newShape.connect(self.newShape) + self.canvas.shapeMoved.connect(self.setDirty) + self.canvas.selectionChanged.connect(self.shapeSelectionChanged) + self.canvas.drawingPolygon.connect(self.toggleDrawingSensitive) + + self.setCentralWidget(scrollArea) + + features = QtWidgets.QDockWidget.DockWidgetFeatures() + for dock in ["flag_dock", "label_dock", "shape_dock", "file_dock"]: + if self._config[dock]["closable"]: + features = features | QtWidgets.QDockWidget.DockWidgetClosable + if self._config[dock]["floatable"]: + features = features | QtWidgets.QDockWidget.DockWidgetFloatable + if self._config[dock]["movable"]: + features = features | QtWidgets.QDockWidget.DockWidgetMovable + getattr(self, dock).setFeatures(features) + if self._config[dock]["show"] is False: + getattr(self, dock).setVisible(False) + + + self.addDockWidget(Qt.RightDockWidgetArea, self.navigation_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.interpolationrefine_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.flag_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.shape_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock) + + # Actions + action = functools.partial(utils.newAction, self) + shortcuts = self._config["shortcuts"] + quit = action( + self.tr("&Quit"), + self.close, + shortcuts["quit"], + "quit", + self.tr("Quit application"), + ) + open_ = action( + self.tr("&Open\n"), + self.openFile, + shortcuts["open"], + "open", + self.tr("Open image or label file"), + ) + opendir = action( + self.tr("Open Dir"), + self.openDirDialog, + shortcuts["open_dir"], + "open", + self.tr("Open Dir"), + ) + openNextImg = action( + self.tr("&Next Image"), + self.openNextImg, + shortcuts["open_next"], + "next", + self.tr("Open next (hold Ctl+Shift to copy labels)"), + enabled=False, + ) + openPrevImg = action( + self.tr("&Prev Image"), + self.openPrevImg, + shortcuts["open_prev"], + "prev", + self.tr("Open prev (hold Ctl+Shift to copy labels)"), + enabled=False, + ) + save = action( + self.tr("&Save\n"), + self.saveFile, + shortcuts["save"], + "save", + self.tr("Save labels to file"), + enabled=False, + ) + saveAs = action( + self.tr("&Save As"), + self.saveFileAs, + shortcuts["save_as"], + "save-as", + self.tr("Save labels to a different file"), + enabled=False, + ) + + deleteFile = action( + self.tr("&Delete File"), + self.deleteFile, + shortcuts["delete_file"], + "delete", + self.tr("Delete current label file"), + enabled=False, + ) + + changeOutputDir = action( + self.tr("&Change Output Dir"), + slot=self.changeOutputDirDialog, + shortcut=shortcuts["save_to"], + icon="open", + tip=self.tr("Change where annotations are loaded/saved"), + ) + + saveAuto = action( + text=self.tr("Save &Automatically"), + slot=lambda x: self.actions.saveAuto.setChecked(x), + icon="save", + tip=self.tr("Save automatically"), + checkable=True, + enabled=True, + ) + saveAuto.setChecked(self._config["auto_save"]) + + saveWithImageData = action( + text="Save With Image Data", + slot=self.enableSaveImageWithData, + tip="Save image data in label file", + checkable=True, + checked=self._config["store_data"], + ) + + close = action( + "&Close", + self.closeFile, + shortcuts["close"], + "close", + "Close current file", + ) + + toggle_keep_prev_mode = action( + self.tr("Keep Previous Annotation"), + self.toggleKeepPrevMode, + shortcuts["toggle_keep_prev_mode"], + None, + self.tr('Toggle "keep pevious annotation" mode'), + checkable=True, + ) + toggle_keep_prev_mode.setChecked(self._config["keep_prev"]) + + createMode = action( + self.tr("Create Polygons"), + lambda: self.toggleDrawMode(False, createMode="polygon"), + shortcuts["create_polygon"], + "objects", + self.tr("Start drawing polygons"), + enabled=False, + ) + createRectangleMode = action( + self.tr("Create Rectangle"), + lambda: self.toggleDrawMode(False, createMode="rectangle"), + shortcuts["create_rectangle"], + "objects", + self.tr("Start drawing rectangles"), + enabled=False, + ) + createCircleMode = action( + self.tr("Create Circle"), + lambda: self.toggleDrawMode(False, createMode="circle"), + shortcuts["create_circle"], + "objects", + self.tr("Start drawing circles"), + enabled=False, + ) + createLineMode = action( + self.tr("Create Line"), + lambda: self.toggleDrawMode(False, createMode="line"), + shortcuts["create_line"], + "objects", + self.tr("Start drawing lines"), + enabled=False, + ) + createPointMode = action( + self.tr("Create Point"), + lambda: self.toggleDrawMode(False, createMode="point"), + shortcuts["create_point"], + "objects", + self.tr("Start drawing points"), + enabled=False, + ) + createLineStripMode = action( + self.tr("Create LineStrip"), + lambda: self.toggleDrawMode(False, createMode="linestrip"), + shortcuts["create_linestrip"], + "objects", + self.tr("Start drawing linestrip. Ctrl+LeftClick ends creation."), + enabled=False, + ) + createAiPolygonMode = action( + self.tr("Create AI-Polygon"), + lambda: self.toggleDrawMode(False, createMode="ai_polygon"), + None, + "objects", + self.tr("Start drawing ai_polygon. Ctrl+LeftClick ends creation."), + enabled=False, + ) + createAiPolygonMode.changed.connect( + lambda: self.canvas.initializeAiModel( + name=self._selectAiModelComboBox.currentText() + ) + if self.canvas.createMode == "ai_polygon" + else None + ) + createAiMaskMode = action( + self.tr("Create AI-Mask"), + lambda: self.toggleDrawMode(False, createMode="ai_mask"), + None, + "objects", + self.tr("Start drawing ai_mask. Ctrl+LeftClick ends creation."), + enabled=False, + ) + createAiMaskMode.changed.connect( + lambda: self.canvas.initializeAiModel( + name=self._selectAiModelComboBox.currentText() + ) + if self.canvas.createMode == "ai_mask" + else None + ) + editMode = action( + self.tr("Edit Polygons"), + self.setEditMode, + shortcuts["edit_polygon"], + "edit", + self.tr("Move and edit the selected polygons"), + enabled=False, + ) + + delete = action( + self.tr("Delete Polygons"), + self.deleteSelectedShape, + shortcuts["delete_polygon"], + "cancel", + self.tr("Delete the selected polygons"), + enabled=False, + ) + duplicate = action( + self.tr("Duplicate Polygons"), + self.duplicateSelectedShape, + shortcuts["duplicate_polygon"], + "copy", + self.tr("Create a duplicate of the selected polygons"), + enabled=False, + ) + copy = action( + self.tr("Copy Polygons"), + self.copySelectedShape, + shortcuts["copy_polygon"], + "copy_clipboard", + self.tr("Copy selected polygons to clipboard"), + enabled=False, + ) + paste = action( + self.tr("Paste Polygons"), + self.pasteSelectedShape, + shortcuts["paste_polygon"], + "paste", + self.tr("Paste copied polygons"), + enabled=False, + ) + undoLastPoint = action( + self.tr("Undo last point"), + self.canvas.undoLastPoint, + shortcuts["undo_last_point"], + "undo", + self.tr("Undo last drawn point"), + enabled=False, + ) + removePoint = action( + text="Remove Selected Point", + slot=self.removeSelectedPoint, + shortcut=shortcuts["remove_selected_point"], + icon="edit", + tip="Remove selected point from polygon", + enabled=False, + ) + + undo = action( + self.tr("Undo\n"), + self.undoShapeEdit, + shortcuts["undo"], + "undo", + self.tr("Undo last add and edit of shape"), + enabled=False, + ) + + hideAll = action( + self.tr("&Hide\nPolygons"), + functools.partial(self.togglePolygons, False), + shortcuts["hide_all_polygons"], + icon="eye", + tip=self.tr("Hide all polygons"), + enabled=False, + ) + showAll = action( + self.tr("&Show\nPolygons"), + functools.partial(self.togglePolygons, True), + shortcuts["show_all_polygons"], + icon="eye", + tip=self.tr("Show all polygons"), + enabled=False, + ) + toggleAll = action( + self.tr("&Toggle\nPolygons"), + functools.partial(self.togglePolygons, None), + shortcuts["toggle_all_polygons"], + icon="eye", + tip=self.tr("Toggle all polygons"), + enabled=False, + ) + + help = action( + self.tr("&Tutorial"), + self.tutorial, + icon="help", + tip=self.tr("Show tutorial page"), + ) + + zoom = QtWidgets.QWidgetAction(self) + zoomBoxLayout = QtWidgets.QVBoxLayout() + zoomLabel = QtWidgets.QLabel("Zoom") + zoomLabel.setAlignment(Qt.AlignCenter) + zoomBoxLayout.addWidget(zoomLabel) + zoomBoxLayout.addWidget(self.zoomWidget) + zoom.setDefaultWidget(QtWidgets.QWidget()) + zoom.defaultWidget().setLayout(zoomBoxLayout) + self.zoomWidget.setWhatsThis( + str( + self.tr( + "Zoom in or out of the image. Also accessible with " + "{} and {} from the canvas." + ) + ).format( + utils.fmtShortcut( + "{},{}".format(shortcuts["zoom_in"], shortcuts["zoom_out"]) + ), + utils.fmtShortcut(self.tr("Ctrl+Wheel")), + ) + ) + self.zoomWidget.setEnabled(False) + + zoomIn = action( + self.tr("Zoom &In"), + functools.partial(self.addZoom, 1.1), + shortcuts["zoom_in"], + "zoom-in", + self.tr("Increase zoom level"), + enabled=False, + ) + zoomOut = action( + self.tr("&Zoom Out"), + functools.partial(self.addZoom, 0.9), + shortcuts["zoom_out"], + "zoom-out", + self.tr("Decrease zoom level"), + enabled=False, + ) + zoomOrg = action( + self.tr("&Original size"), + functools.partial(self.setZoom, 100), + shortcuts["zoom_to_original"], + "zoom", + self.tr("Zoom to original size"), + enabled=False, + ) + keepPrevScale = action( + self.tr("&Keep Previous Scale"), + self.enableKeepPrevScale, + tip=self.tr("Keep previous zoom scale"), + checkable=True, + checked=self._config["keep_prev_scale"], + enabled=True, + ) + fitWindow = action( + self.tr("&Fit Window"), + self.setFitWindow, + shortcuts["fit_window"], + "fit-window", + self.tr("Zoom follows window size"), + checkable=True, + enabled=False, + ) + fitWidth = action( + self.tr("Fit &Width"), + self.setFitWidth, + shortcuts["fit_width"], + "fit-width", + self.tr("Zoom follows window width"), + checkable=True, + enabled=False, + ) + brightnessContrast = action( + "&Brightness Contrast", + self.brightnessContrast, + None, + "color", + "Adjust brightness and contrast", + enabled=False, + ) + # Group zoom controls into a list for easier toggling. + zoomActions = ( + self.zoomWidget, + zoomIn, + zoomOut, + zoomOrg, + fitWindow, + fitWidth, + ) + self.zoomMode = self.FIT_WINDOW + fitWindow.setChecked(Qt.Checked) + self.scalers = { + self.FIT_WINDOW: self.scaleFitWindow, + self.FIT_WIDTH: self.scaleFitWidth, + # Set to one to scale to 100% when loading files. + self.MANUAL_ZOOM: lambda: 1, + } + + edit = action( + self.tr("&Edit Label"), + self.editLabel, + shortcuts["edit_label"], + "edit", + self.tr("Modify the label of the selected polygon"), + enabled=False, + ) + + edit_ID = action( + self.tr("&Edit ID"), + self.editID, + shortcuts["edit_id"], + "edit", + self.tr("Modify the ID of the selected polygon"), + enabled=False + ) + + call_sort = action( + self.tr("&ID Association"), + self.SORT, + None, + "edit", + self.tr("ID Association"), + enabled=False, + ) + + call_interpolation = action( + self.tr("&Box/ID Interpolation"), + self.INTERPOLATION, + None, + "edit", + self.tr("Box/ID Interpolation"), + enabled=False, + ) + + call_deletion = action( + self.tr("&Box/ID Modification"), + self.DELETION, + None, + "edit", + self.tr("Box/ID Modification"), + enabled=False, + ) + + fill_drawing = action( + self.tr("Fill Drawing Polygon"), + self.canvas.setFillDrawing, + None, + "color", + self.tr("Fill polygon while drawing"), + checkable=True, + enabled=True, + ) + if self._config["canvas"]["fill_drawing"]: + fill_drawing.trigger() + + # Lavel list context menu. + labelMenu = QtWidgets.QMenu() + utils.addActions(labelMenu, (edit, delete)) + self.labelList.setContextMenuPolicy(Qt.CustomContextMenu) + self.labelList.customContextMenuRequested.connect(self.popLabelListMenu) + + IDMenu = QtWidgets.QMenu() + utils.addActions(IDMenu, (edit_ID, delete)) + self.IDList.setContextMenuPolicy(Qt.CustomContextMenu) + self.IDList.customContextMenuRequested.connect(self.popIDListMenu) + + # Store actions for further handling. + self.actions = utils.struct( + saveAuto=saveAuto, + saveWithImageData=saveWithImageData, + changeOutputDir=changeOutputDir, + save=save, + saveAs=saveAs, + open=open_, + close=close, + deleteFile=deleteFile, + toggleKeepPrevMode=toggle_keep_prev_mode, + delete=delete, + edit=edit, + edit_id=edit_ID, + SORT=call_sort, + INPO=call_interpolation, + DELE=call_deletion, + duplicate=duplicate, + copy=copy, + paste=paste, + undoLastPoint=undoLastPoint, + undo=undo, + removePoint=removePoint, + createMode=createMode, + editMode=editMode, + createRectangleMode=createRectangleMode, + createCircleMode=createCircleMode, + createLineMode=createLineMode, + createPointMode=createPointMode, + createLineStripMode=createLineStripMode, + createAiPolygonMode=createAiPolygonMode, + createAiMaskMode=createAiMaskMode, + zoom=zoom, + zoomIn=zoomIn, + zoomOut=zoomOut, + zoomOrg=zoomOrg, + keepPrevScale=keepPrevScale, + fitWindow=fitWindow, + fitWidth=fitWidth, + brightnessContrast=brightnessContrast, + zoomActions=zoomActions, + openNextImg=openNextImg, + openPrevImg=openPrevImg, + fileMenuActions=(open_, opendir, save, saveAs, close, quit), + tool=(), + # XXX: need to add some actions here to activate the shortcut + editMenu=( + edit, + edit_ID, + duplicate, + copy, + paste, + delete, + None, + undo, + undoLastPoint, + None, + removePoint, + None, + toggle_keep_prev_mode, + ), + # menu shown at right click + menu=( + createMode, + createRectangleMode, + createCircleMode, + createLineMode, + createPointMode, + createLineStripMode, + createAiPolygonMode, + createAiMaskMode, + editMode, + edit, + edit_ID, + duplicate, + copy, + paste, + delete, + undo, + undoLastPoint, + removePoint, + ), + onLoadActive=( + close, + createMode, + createRectangleMode, + createCircleMode, + createLineMode, + createPointMode, + createLineStripMode, + createAiPolygonMode, + createAiMaskMode, + editMode, + brightnessContrast, + ), + onShapesPresent=(saveAs, hideAll, showAll, toggleAll), + ) + + self.canvas.vertexSelected.connect(self.actions.removePoint.setEnabled) + + self.menus = utils.struct( + file=self.menu(self.tr("&File")), + edit=self.menu(self.tr("&Edit")), + track=self.menu(self.tr("&Track")), + view=self.menu(self.tr("&View")), + help=self.menu(self.tr("&Help")), + recentFiles=QtWidgets.QMenu(self.tr("Open &Recent")), + labelList=labelMenu, + IDList=IDMenu + ) + + utils.addActions( + self.menus.file, + ( + open_, + openNextImg, + openPrevImg, + opendir, + self.menus.recentFiles, + save, + saveAs, + saveAuto, + changeOutputDir, + saveWithImageData, + close, + deleteFile, + None, + quit, + ), + ) + utils.addActions(self.menus.help, (help,)) + utils.addActions( + self.menus.view, + ( + self.flag_dock.toggleViewAction(), + self.label_dock.toggleViewAction(), + self.shape_dock.toggleViewAction(), + self.file_dock.toggleViewAction(), + None, + fill_drawing, + None, + hideAll, + showAll, + toggleAll, + None, + zoomIn, + zoomOut, + zoomOrg, + keepPrevScale, + None, + fitWindow, + fitWidth, + None, + brightnessContrast, + ), + ) + utils.addActions( + self.menus.track, + (call_interpolation,call_sort,call_deletion), + ) + + self.menus.file.aboutToShow.connect(self.updateFileMenu) + + # Custom context menu for the canvas widget: + utils.addActions(self.canvas.menus[0], self.actions.menu) + # utils.addActions(self.canvas.editID) + utils.addActions( + self.canvas.menus[1], + ( + action("&Copy here", self.copyShape), + action("&Move here", self.moveShape), + ), + ) + + selectAiModel = QtWidgets.QWidgetAction(self) + selectAiModel.setDefaultWidget(QtWidgets.QWidget()) + selectAiModel.defaultWidget().setLayout(QtWidgets.QVBoxLayout()) + # + selectAiModelLabel = QtWidgets.QLabel(self.tr("AI Model")) + selectAiModelLabel.setAlignment(QtCore.Qt.AlignCenter) + selectAiModel.defaultWidget().layout().addWidget(selectAiModelLabel) + # + self._selectAiModelComboBox = QtWidgets.QComboBox() + selectAiModel.defaultWidget().layout().addWidget(self._selectAiModelComboBox) + model_names = [model.name for model in MODELS] + self._selectAiModelComboBox.addItems(model_names) + if self._config["ai"]["default"] in model_names: + model_index = model_names.index(self._config["ai"]["default"]) + else: + logger.warning( + "Default AI model is not found: %r", + self._config["ai"]["default"], + ) + model_index = 0 + self._selectAiModelComboBox.setCurrentIndex(model_index) + self._selectAiModelComboBox.currentIndexChanged.connect( + lambda: self.canvas.initializeAiModel( + name=self._selectAiModelComboBox.currentText() + ) + if self.canvas.createMode in ["ai_polygon", "ai_mask"] + else None + ) + + self.tools = self.toolbar("Tools") + self.actions.tool = ( + open_, + opendir, + openPrevImg, + openNextImg, + save, + deleteFile, + None, + createMode, + editMode, + duplicate, + delete, + undo, + brightnessContrast, + None, + fitWindow, + zoom, + None, + selectAiModel, + ) + + self.statusBar().showMessage(str(self.tr("%s started.")) % __appname__) + self.statusBar().show() + + if output_file is not None and self._config["auto_save"]: + logger.warn( + "If `auto_save` argument is True, `output_file` argument " + "is ignored and output filename is automatically " + "set as IMAGE_BASENAME.json." + ) + self.output_file = output_file + self.output_dir = output_dir + + # Application state. + self.image = QtGui.QImage() + self.imagePath = None + self.recentFiles = [] + self.maxRecent = 7 + self.otherData = None + self.zoom_level = 100 + self.fit_window = False + self.zoom_values = {} # key=filename, value=(zoom_mode, zoom_value) + self.brightnessContrast_values = {} + self.scroll_values = { + Qt.Horizontal: {}, + Qt.Vertical: {}, + } # key=filename, value=scroll_value + + if filename is not None and osp.isdir(filename): + self.importDirImages(filename, load=False) + else: + self.filename = filename + + if config["file_search"]: + self.fileSearch.setText(config["file_search"]) + self.fileSearchChanged() + + # XXX: Could be completely declarative. + # Restore application settings. + self.settings = QtCore.QSettings("labelme", "labelme") + self.recentFiles = self.settings.value("recentFiles", []) or [] + size = self.settings.value("window/size", QtCore.QSize(600, 500)) + position = self.settings.value("window/position", QtCore.QPoint(0, 0)) + state = self.settings.value("window/state", QtCore.QByteArray()) + self.resize(size) + self.move(position) + # or simply: + # self.restoreGeometry(settings['window/geometry'] + self.restoreState(state) + + # Populate the File menu dynamically. + self.updateFileMenu() + # Since loading the file may take some time, + # make sure it runs in the background. + if self.filename is not None: + self.queueEvent(functools.partial(self.loadFile, self.filename)) + + # Callbacks: + self.zoomWidget.valueChanged.connect(self.paintCanvas) + + self.populateModeActions() + + # self.firstStart = True + # if self.firstStart: + # QWhatsThis.enterWhatsThisMode() + + def menu(self, title, actions=None): + menu = self.menuBar().addMenu(title) + if actions: + utils.addActions(menu, actions) + return menu + + def toolbar(self, title, actions=None): + toolbar = ToolBar(title) + toolbar.setObjectName("%sToolBar" % title) + # toolbar.setOrientation(Qt.Vertical) + toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + if actions: + utils.addActions(toolbar, actions) + self.addToolBar(Qt.TopToolBarArea, toolbar) + return toolbar + + # Support Functions + + def noShapes(self): + return not len(self.labelList) + + def populateModeActions(self): + tool, menu = self.actions.tool, self.actions.menu + self.tools.clear() + utils.addActions(self.tools, tool) + self.canvas.menus[0].clear() + utils.addActions(self.canvas.menus[0], menu) + self.menus.edit.clear() + actions = ( + self.actions.createMode, + self.actions.createRectangleMode, + self.actions.createCircleMode, + self.actions.createLineMode, + self.actions.createPointMode, + self.actions.createLineStripMode, + self.actions.createAiPolygonMode, + self.actions.createAiMaskMode, + self.actions.editMode, + ) + utils.addActions(self.menus.edit, actions + self.actions.editMenu) + + def setDirty(self): + # Even if we autosave the file, we keep the ability to undo + self.actions.undo.setEnabled(self.canvas.isShapeRestorable) + if self._config["auto_save"] or self.actions.saveAuto.isChecked(): + label_file = osp.splitext(self.imagePath)[0] + ".json" + if self.output_dir: + label_file_without_path = osp.basename(label_file) + label_file = osp.join(self.output_dir, label_file_without_path) + self.saveLabels(label_file) + return + self.dirty = True + self.actions.save.setEnabled(True) + title = __appname__ + if self.filename is not None: + title = "{} - {}*".format(title, self.filename) + self.setWindowTitle(title) + + def setClean(self): + self.dirty = False + self.actions.save.setEnabled(False) + self.actions.createMode.setEnabled(True) + self.actions.createRectangleMode.setEnabled(True) + self.actions.createCircleMode.setEnabled(True) + self.actions.createLineMode.setEnabled(True) + self.actions.createPointMode.setEnabled(True) + self.actions.createLineStripMode.setEnabled(True) + self.actions.createAiPolygonMode.setEnabled(True) + self.actions.createAiMaskMode.setEnabled(True) + self.actions.SORT.setEnabled(True) + self.actions.INPO.setEnabled(True) + self.actions.DELE.setEnabled(True) + title = __appname__ + if self.filename is not None: + title = "{} - {}".format(title, self.filename) + self.setWindowTitle(title) + + if self.hasLabelFile(): + self.actions.deleteFile.setEnabled(True) + else: + self.actions.deleteFile.setEnabled(False) + + def toggleActions(self, value=True): + """Enable/Disable widgets which depend on an opened image.""" + for z in self.actions.zoomActions: + z.setEnabled(value) + for action in self.actions.onLoadActive: + action.setEnabled(value) + + def queueEvent(self, function): + QtCore.QTimer.singleShot(0, function) + + def status(self, message, delay=5000): + self.statusBar().showMessage(message, delay) + + def resetState(self): + self.labelList.clear() + self.IDList.clear() + self.filename = None + self.imagePath = None + self.imageData = None + self.labelFile = None + self.otherData = None + self.canvas.resetState() + + def currentItem(self): + items_l = self.labelList.selectedItems() + items_i = self.IDList.selectedItems() + if items_l: + return items_l[0], items_i[0] + return None, None + + def addRecentFile(self, filename): + if filename in self.recentFiles: + self.recentFiles.remove(filename) + elif len(self.recentFiles) >= self.maxRecent: + self.recentFiles.pop() + self.recentFiles.insert(0, filename) + + # Callbacks + + def undoShapeEdit(self): + self.canvas.restoreShape() + self.labelList.clear() + self.IDList.clear() + self.loadShapes(self.canvas.shapes) + self.actions.undo.setEnabled(self.canvas.isShapeRestorable) + + def tutorial(self): + url = "https://github.com/wkentaro/labelme/tree/main/examples/tutorial" # NOQA + webbrowser.open(url) + + def toggleDrawingSensitive(self, drawing=True): + """Toggle drawing sensitive. + + In the middle of drawing, toggling between modes should be disabled. + """ + self.actions.editMode.setEnabled(not drawing) + self.actions.undoLastPoint.setEnabled(drawing) + self.actions.undo.setEnabled(not drawing) + self.actions.delete.setEnabled(not drawing) + + def toggleDrawMode(self, edit=True, createMode="polygon"): + draw_actions = { + "polygon": self.actions.createMode, + "rectangle": self.actions.createRectangleMode, + "circle": self.actions.createCircleMode, + "point": self.actions.createPointMode, + "line": self.actions.createLineMode, + "linestrip": self.actions.createLineStripMode, + "ai_polygon": self.actions.createAiPolygonMode, + "ai_mask": self.actions.createAiMaskMode, + } + + self.canvas.setEditing(edit) + self.canvas.createMode = createMode + if edit: + for draw_action in draw_actions.values(): + draw_action.setEnabled(True) + else: + for draw_mode, draw_action in draw_actions.items(): + draw_action.setEnabled(createMode != draw_mode) + self.actions.editMode.setEnabled(not edit) + + def setEditMode(self): + self.toggleDrawMode(True) + + def updateFileMenu(self): + current = self.filename + + def exists(filename): + return osp.exists(str(filename)) + + menu = self.menus.recentFiles + menu.clear() + files = [f for f in self.recentFiles if f != current and exists(f)] + for i, f in enumerate(files): + icon = utils.newIcon("labels") + action = QtWidgets.QAction( + icon, "&%d %s" % (i + 1, QtCore.QFileInfo(f).fileName()), self + ) + action.triggered.connect(functools.partial(self.loadRecent, f)) + menu.addAction(action) + + def popLabelListMenu(self, point): + self.menus.labelList.exec_(self.labelList.mapToGlobal(point)) + + def popIDListMenu(self, point): + self.menus.IDList.exec_(self.IDList.mapToGlobal(point)) + + def validateLabel(self, label): + # no validation + if self._config["validate_label"] is None: + return True + + for i in range(self.uniqLabelList.count()): + label_i = self.uniqLabelList.item(i).data(Qt.UserRole) + if self._config["validate_label"] in ["exact"]: + if label_i == label: + return True + return False + + def editIR_info(self, item=None): + dialog = InterpolationRefineInfo_Dialog( + parent=self, + ) + dialog.exec_() + + self.ir_name = dialog.name + self.ir_id = dialog.id + + self.interpolationrefine_list.statusBar.showMessage(f'Name: {self.ir_name} | ID: {self.ir_id}') + + + def SORT(self, item=None): + def convert(box): + w = abs(box[1][0] - box[0][0]) + h = abs(box[1][1] - box[0][1]) + return [box[0][0],box[0][1],w,h] + + dialog = TrackDialog( + parent=self, + ) + dialog.exec_() + + # 3 modes + # + start by using the current frame's track annotation + # + start from scratch without track annotation + track_option = dialog.option_value + + # print frame shortage here + # ... + + if track_option != 0: + # get the index of current item + items = self.fileListWidget.selectedItems() + item = items[0] + currIndex = self.imageList.index(str(item.text())) + + # get label list .json + if track_option == 1: # start from beginning + labelList = [osp.splitext(img_p)[0] + ".json" for img_p in self.imageList] + else: + labelList = [osp.splitext(img_p)[0] + ".json" for img_p in self.imageList][currIndex:] + + # convert label to Track Evaluation + lines = [] + frame_id = 1 + for jdx, json_path in enumerate(labelList): + if jdx == 0: # first frame + bboxes = [ [[self.IDList[idx].shape().points[0].x(),\ + self.IDList[idx].shape().points[0].y()],\ + [self.IDList[idx].shape().points[1].x(),\ + self.IDList[idx].shape().points[1].y()]] for idx in range(len(self.IDList))] + bboxes_xywh = [convert(bboxes[i]) for i in range(len(bboxes))] + + if track_option == 2: # start with current annotation + track_ids = [self.IDList[idx].text() for idx in range(len(self.IDList))] + int_track_ids = [int(self.IDList[idx].text()) for idx in range(len(self.IDList))] + + if '-1' in track_ids: + self.errorMessage( + "Track IDs", + "You must label all objects' IDs.", + ) + return + for l in range(len(bboxes_xywh)): + lines.append([frame_id,int(track_ids[l]),bboxes_xywh[l][0],bboxes_xywh[l][1],bboxes_xywh[l][2],bboxes_xywh[l][3],1,-1,-1,-1]) + else: # start with NO annotation + for l in range(len(bboxes_xywh)): + lines.append([frame_id,-1,bboxes_xywh[l][0],bboxes_xywh[l][1],bboxes_xywh[l][2],bboxes_xywh[l][3],1,-1,-1,-1]) + else: # next frames # all ids are -1 + with open(json_path) as file: + data = json.load(file) + + bboxes = [data['shapes'][i]['points'] for i in range(len(data['shapes']))] + # need boxes in x,y,w,h + bboxes_xywh = [convert(bboxes[i]) for i in range(len(bboxes))] + + # frame_id, track_id, x_top_left, y_top_left, width, height,1,-1,-1,-1 + for l in range(len(bboxes_xywh)): + lines.append([frame_id,-1,bboxes_xywh[l][0],bboxes_xywh[l][1],bboxes_xywh[l][2],bboxes_xywh[l][3],1,-1,-1,-1]) + frame_id += 1 + seq_dets = np.array(lines).astype(float) + + mot_tracker = SORT_main(max_age=10,min_hits=0,iou_threshold=0.3) + mot_tracker.trackers = [] + KalmanBoxTracker.count = 0 + track_results = [] + if track_option == 2: + for idx in range(len(seq_dets[seq_dets[:, 0]==1])): + dets = seq_dets[seq_dets[:, 0]==1,2:7][idx:idx+1] # original setting: x,y,w,h top left width height + det_id = seq_dets[seq_dets[:, 0]==1,1][idx] + dets[:, 2:4] += dets[:, 0:2] #convert to [x1,y1,w,h] to [x1,y1,x2,y2] + KalmanBoxTracker.count = max(int_track_ids)+1 + KalmanBoxTracker.id = 0 + mot_tracker.trackers.append(KalmanBoxTracker(dets[0],id=det_id)) + + for frame in range(int(seq_dets[:,0].max())): + frame += 1 #detection and frame numbers begin at 1 + dets = seq_dets[seq_dets[:, 0]==frame, 2:7] # original setting: x,y,w,h top left width height + dets[:, 2:4] += dets[:, 0:2] #convert to [x1,y1,w,h] to [x1,y1,x2,y2] + trackers = mot_tracker.update(dets) + + for d in trackers: + track_results.append([frame,d[4],d[0],d[1],d[2]-d[0],d[3]-d[1]]) + track_results = np.array(track_results).astype(int) + + lf = LabelFile() + flags = {} + # Edit frame Shape and save + for jdx, json_path in enumerate(labelList): + loaded_label = LabelFile(json_path) + loaded_shape = loaded_label.shapes + frame = jdx+1 + track_ids = track_results[track_results[:, 0]==frame,1] + track_xy = track_results[track_results[:, 0]==frame,2:4] + shape_xy = [loaded_shape[idx]['points'][0] for idx in range(len(loaded_shape))] + + hungarian_matrix = [] + for adx in range(len(shape_xy)): + row = [] + for bdx in range(len(track_xy)): + row.append( np.sum(np.abs(shape_xy[adx]-track_xy[bdx])) ) + hungarian_matrix.append(row) + shape_m,track_m = linear_sum_assignment(np.array(hungarian_matrix)) + + for idx in range(len(shape_xy)): + loaded_shape[idx]['track_id'] = str(track_ids[track_m[idx]]) + + imagePath = osp.splitext(json_path)[0] + ".jpg" + lf.save( + filename=json_path, + shapes=loaded_shape, + imagePath=imagePath, + imageData=None, + imageHeight=self.image.height(), + imageWidth=self.image.width(), + flags=flags, + ) + + # repaint the current frame + self.informationMessage( + "Track IDs", + "ID Association is completed", + ) + self.loadFile(self.filename) + + def INTERPOLATION(self, item=None): + dialog = InterpolationDialog( + min_val=0,max_val=len(self.imageList),parent=self + ) + dialog.exec_() + # import ipdb; ipdb.set_trace() + if (dialog.start_frame_cell.text() == '' or + dialog.end_frame_cell.text() == '' or + dialog.interval_cell.text() == '' or + dialog.ID_cell.text() == '' or + dialog.label_cell.text() == ''): + + self.errorMessage( + "Box Interpolation", + "You must fill all the values in the form.", + ) + + return + + self.start_INP0 = start_frame = int(dialog.start_frame_cell.text().replace(" ", "")) + self.end_INP0 = end_frame = int(dialog.end_frame_cell.text().replace(" ", "")) + self.interval_INPO = interval = int(dialog.interval_cell.text().replace(" ", "")) + self.ID_INPO = ID = dialog.ID_cell.text().replace(" ", "") + self.label_INPO = label = dialog.label_cell.text().replace(" ", "") + + if end_frame - start_frame <=0: + self.errorMessage( + "Box Interpolation", + "Start frame is higher than End frame.", + ) + + return + elif interval == 0 or interval > (end_frame-start_frame): + self.errorMessage( + "Box Interpolation", + "Input Interval is bigger than Start-End frame gap (or is 0).", + ) + + return + elif end_frame > len(self.imageList): + self.errorMessage( + "Box Interpolation", + "Input End frame is out of the video length.", + ) + + return + + img_indices = np.linspace(start_frame-1,end_frame-1,num=int((end_frame-start_frame+1)/interval),dtype=int) + + self.mode = "TRACK INTERPOLATION" + self.INTERPOLATION_list = np.array(self.imageList)[img_indices].tolist() + self.INTERPOLATION_filename = self.INTERPOLATION_list[0] + self.filename = self.INTERPOLATION_filename + self.loadFile(self.filename) + + def OKAY(self, item=None): + def convert(box): + return [box[0][0],box[0][1],box[1][0],box[1][1]] + + def cvt_xyxy2xywh(old_bboxes): + new_bboxes = np.zeros(old_bboxes.shape) + new_bboxes[:,0] = (old_bboxes[:,0]+old_bboxes[:,2])/2 + new_bboxes[:,1] = (old_bboxes[:,1]+old_bboxes[:,3])/2 + new_bboxes[:,2] = old_bboxes[:,2] - old_bboxes[:,0] + new_bboxes[:,3] = old_bboxes[:,3] - old_bboxes[:,1] + return new_bboxes + + def cvt_xywh2xyxy(old_bboxes): + new_bboxes = np.zeros(old_bboxes.shape) + dw = old_bboxes[:,2]/2 + dh = old_bboxes[:,3]/2 + new_bboxes[:,0] = old_bboxes[:,0] - dw + new_bboxes[:,1] = old_bboxes[:,1] - dh + new_bboxes[:,2] = old_bboxes[:,0] + dw + new_bboxes[:,3] = old_bboxes[:,1] + dh + return new_bboxes + + from sklearn.gaussian_process import GaussianProcessRegressor + from sklearn.gaussian_process.kernels import RationalQuadratic + + if self.mode == "None" or self.mode == "NORMAL": + return + elif self.mode == "TRACK INTERPOLATION": + self.INTERPOLATION_filename = self.INTERPOLATION_list[0] + self.filename = self.INTERPOLATION_filename + self.loadFile(self.filename) + + # print(self.start_INP0, self.end_INP0, self.ID_INPO, self.label_INPO) + + labelList = [osp.splitext(img_p)[0] + ".json" for img_p in self.INTERPOLATION_list] + interpolatedList = self.imageList[self.start_INP0-1:self.end_INP0] + interpolatedList = [osp.splitext(img_p)[0] + ".json" for img_p in interpolatedList] + img_indices = np.linspace(self.start_INP0-1,self.end_INP0-1,num=int((self.end_INP0-self.start_INP0+1)/self.interval_INPO),dtype=int) + + ref_bxoxes = [] + for jdx, json_path in enumerate(labelList): + with open(json_path) as file: + data = json.load(file) + + + bboxes = [data['shapes'][i]['points'] for i in range(len(data['shapes']))] + labels = [data['shapes'][i]['label'] for i in range(len(data['shapes']))] + track_ids = [data['shapes'][i]['track_id'] for i in range(len(data['shapes']))] + # need boxes in x,y,w,h + bboxes_xyxy = [convert(bboxes[i]) for i in range(len(bboxes))] + picked_item = np.intersect1d(np.argwhere(np.array(track_ids)==self.ID_INPO), + np.argwhere(np.array(labels)==self.label_INPO))[0] + ref_bxoxes.append(bboxes_xyxy[picked_item]) + + xyxy_bboxes = np.array(ref_bxoxes).astype(int) + xywh_bboxes = cvt_xyxy2xywh(xyxy_bboxes) + + interpolated_data = [] + for jdx in range(4): + kernel = RationalQuadratic() + gpr = GaussianProcessRegressor(kernel=kernel,random_state=0).fit(img_indices.reshape(-1,1), xywh_bboxes[:,jdx]) + interpolated_data.append(gpr.predict(np.arange(self.start_INP0-1,self.end_INP0).reshape(-1,1), return_std=False)) + + interpolated_data = np.stack(interpolated_data,axis=1) + cvt_interpolated_data = cvt_xywh2xyxy(interpolated_data).astype(int) + + lf = LabelFile() + # Edit frame Shape and save + for jdx, json_path in enumerate(interpolatedList): + if json_path in labelList: + continue + if os.path.isfile(json_path): + loaded_label = LabelFile(json_path) + loaded_shape = loaded_label.shapes + newshape = { + 'label':self.label_INPO, + 'points':[[int(cvt_interpolated_data[jdx][0]),int(cvt_interpolated_data[jdx][1])],[int(cvt_interpolated_data[jdx][2]),int(cvt_interpolated_data[jdx][3])]], + 'shape_type':'rectangle', + 'flags':{}, + 'description':'', + 'group_id': None, + 'track_id': self.ID_INPO, + 'mask': None + } + loaded_shape.append(newshape) + + imagePath = osp.splitext(json_path)[0] + ".jpg" + # import ipdb; ipdb.set_trace() + lf.save( + filename=json_path, + shapes=loaded_shape, + imagePath=imagePath, + imageData=None, + imageHeight=self.image.height(), + imageWidth=self.image.width(), + otherData={}, + flags={}, + ) + else: + loaded_shape = [] + newshape = { + 'label':self.label_INPO, + 'points':[[int(cvt_interpolated_data[jdx][0]),int(cvt_interpolated_data[jdx][1])],[int(cvt_interpolated_data[jdx][2]),int(cvt_interpolated_data[jdx][3])]], + 'shape_type':'rectangle', + 'flags':{}, + 'description':'', + 'group_id': None, + 'track_id': self.ID_INPO, + 'mask': None + } + loaded_shape.append(newshape) + imagePath = osp.splitext(json_path)[0] + ".jpg" + lf.save( + filename=json_path, + shapes=loaded_shape, + imagePath=imagePath, + imageData=None, + imageHeight=self.image.height(), + imageWidth=self.image.width(), + otherData={}, + flags={}, + ) + + filenames = self.scanAllImages(self.lastOpenDir) + + for filename in filenames: + label_file = osp.splitext(filename)[0] + ".json" + label_index = self.imageList.index(filename) + item = self.fileListWidget.item(label_index) + if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): + item.setCheckState(Qt.Checked) + + # repaint the current frame + self.mode = "NORMAL" + self.INTERPOLATION_filename = self.INTERPOLATION_list[0] + self.filename = self.INTERPOLATION_filename + # load the start file + getIndex = self.imageList.index(self.INTERPOLATION_filename) + 1 + self.navigation_list.statusBar.showMessage(f'Status: {getIndex}/{len(self.imageList)} | Mode: {self.mode}') + self.loadFile(self.filename) + + self.informationMessage( + "Box Interpolation", + f"Track {self.label_INPO}-{self.ID_INPO} from frame {self.start_INP0} to {self.end_INP0} Interpolation is completed", + ) + + def DELETION(self, item=None): + dialog = DeletionDialog( + parent=self, + ) + dialog.exec_() + + if (dialog.start_frame_cell.text() == '' or + dialog.end_frame_cell.text() == '' or + dialog.ID_cell.text() == '' or + dialog.label_cell.text() == ''): + return + + start_frame = int(dialog.start_frame_cell.text().replace(" ", "")) + end_frame = int(dialog.end_frame_cell.text().replace(" ", "")) + ID = dialog.ID_cell.text().replace(" ", "") + label = dialog.label_cell.text().replace(" ", "") + + if end_frame - start_frame <=0: + self.errorMessage( + "Track Deletion", + "Start frame is higher than End frame.", + ) + + return + + labelList = [osp.splitext(img_p)[0] + ".json" for img_p in self.imageList] + deletionList = labelList[start_frame-1:end_frame] + + lf = LabelFile() + # Edit frame Shape and save + for jdx, json_path in enumerate(deletionList): + loaded_label = LabelFile(json_path) + loaded_shape = loaded_label.shapes + new_shape = [] + for kdx in range(len(loaded_shape)): + if loaded_shape[kdx]['label'] == label and loaded_shape[kdx]['track_id'] ==ID: + continue + else: + new_shape.append(loaded_shape[kdx]) + + imagePath = osp.splitext(json_path)[0] + ".jpg" + + lf.save( + filename=json_path, + shapes=new_shape, + imagePath=imagePath, + imageData=None, + imageHeight=self.image.height(), + imageWidth=self.image.width(), + flags={}, + ) + + + self.filename = self.imageList[start_frame] + # load the start file + getIndex = self.imageList.index(self.filename) + 1 + self.navigation_list.statusBar.showMessage(f'Status: {getIndex}/{len(self.imageList)} | Mode: {self.mode}') + self.loadFile(self.filename) + + self.informationMessage( + "Track Deletion", + f"Track {label}-{ID} from frame {start_frame} to {end_frame} is deleted", + ) + + + def editID(self, item=None): + if item and not isinstance(item, IDListWidgetItem): + raise TypeError("item must be IDListWidgetItem type") + + if not self.canvas.editing(): + return + if not item: + _,item = self.currentItem() + if item is None: + return + shape = item.shape() + if shape is None: + return + id = self.IDDialog.popUp( + text=shape.track_id + ) + if id is None: + return + if not self.validateLabel(id): + self.errorMessage( + self.tr("Invalid ID"), + self.tr("Invalid ID '{}' with validation type '{}'").format( + id, self._config["validate_label"] + ), + ) + return + shape.track_id = id + + item.setText(shape.track_id) + self._update_shape_color(shape) + self.setDirty() + unique_name = shape.label + '_' + str(shape.track_id) + if self.uniqLabelList.findItemByLabel(unique_name) is None: + item = self.uniqLabelList.createItemFromLabel(unique_name) + self.uniqLabelList.addItem(item) + rgb = self._get_rgb_by_label(unique_name) + self.uniqLabelList.setItemLabel(item, unique_name, rgb) + + def editLabel(self, item=None): + if item and not isinstance(item, LabelListWidgetItem): + raise TypeError("item must be LabelListWidgetItem type") + if not self.canvas.editing(): + return + if not item: + item,_ = self.currentItem() + if item is None: + return + shape = item.shape() + if shape is None: + return + text, flags, group_id, description = self.labelDialog.popUp( + text=shape.label, + flags=shape.flags, + group_id=shape.group_id, + description=shape.description, + ) + if text is None: + return + if not self.validateLabel(text): + self.errorMessage( + self.tr("Invalid label"), + self.tr("Invalid label '{}' with validation type '{}'").format( + text, self._config["validate_label"] + ), + ) + return + shape.label = text + shape.flags = flags + shape.group_id = group_id + shape.description = description + + self._update_shape_color(shape) + if shape.group_id is None: + item.setText( + '{} '.format( + html.escape(shape.label), *shape.fill_color.getRgb()[:3] + ) + ) + else: + item.setText("{} ({})".format(shape.label, shape.group_id)) + self.setDirty() + unique_name = shape.label + '_' + str(shape.track_id) + if self.uniqLabelList.findItemByLabel(unique_name) is None: + item = self.uniqLabelList.createItemFromLabel(unique_name) + self.uniqLabelList.addItem(item) + rgb = self._get_rgb_by_label(unique_name) + self.uniqLabelList.setItemLabel(item, unique_name, rgb) + + def fileSearchChanged(self): + self.importDirImages( + self.lastOpenDir, + pattern=self.fileSearch.text(), + load=False, + ) + + def fileSelectionChanged(self): + items = self.fileListWidget.selectedItems() + if not items: + return + item = items[0] + + if not self.mayContinue(): + return + + if self.mode == "None": + self.mode = "NORMAL" + + if self.mode == "TRACK INTERPOLATION": + currIndex = self.imageList.index(str(item.text())) + if currIndex < len(self.imageList): + filename = self.imageList[currIndex] + + if filename in self.INTERPOLATION_list: + getIndex = self.imageList.index(filename) + 1 + interpolationIndex = self.INTERPOLATION_list.index(self.filename) + 1 + self.navigation_list.statusBar.showMessage(f'Status: {getIndex}/{len(self.imageList)} | Mode: {self.mode} - ({interpolationIndex}/{len(self.INTERPOLATION_list)})') + + self.loadFile(filename) + else: + self.errorMessage( + "Box Interpolation", + "You cannot select out-of-list frame. Use Previous (A) and Next (D) buttons to move between the selected frames", + ) + return + + else: + currIndex = self.imageList.index(str(item.text())) + if currIndex < len(self.imageList): + filename = self.imageList[currIndex] + if filename: + self.loadFile(filename) + + getIndex = self.imageList.index(self.filename) + 1 + self.navigation_list.statusBar.showMessage(f'Status: {getIndex}/{len(self.imageList)} | Mode: {self.mode}') + + # React to canvas signals. + def shapeSelectionChanged(self, selected_shapes): + self._noSelectionSlot = True + for shape in self.canvas.selectedShapes: + shape.selected = False + self.labelList.clearSelection() + self.IDList.clearSelection() + self.canvas.selectedShapes = selected_shapes + for shape in self.canvas.selectedShapes: + shape.selected = True + item = self.labelList.findItemByShape(shape) + self.labelList.selectItem(item) + self.labelList.scrollToItem(item) + id_item = self.IDList.findItemByShape(shape) + self.IDList.selectItem(id_item) + self.IDList.scrollToItem(id_item) + self._noSelectionSlot = False + n_selected = len(selected_shapes) + self.actions.delete.setEnabled(n_selected) + self.actions.duplicate.setEnabled(n_selected) + self.actions.copy.setEnabled(n_selected) + self.actions.edit.setEnabled(n_selected == 1) + self.actions.edit_id.setEnabled(n_selected == 1) + + def addLabel(self, shape): + if shape.group_id is None: + text = shape.label + else: + text = "{} ({})".format(shape.label, shape.group_id) + label_list_item = LabelListWidgetItem(text, shape) + self.labelList.addItem(label_list_item) + id_list_item = IDListWidgetItem(str(shape.track_id),shape) + self.IDList.addItem(id_list_item) + unique_name = shape.label + '_' + str(shape.track_id) + if self.uniqLabelList.findItemByLabel(unique_name) is None: + item = self.uniqLabelList.createItemFromLabel(unique_name) + self.uniqLabelList.addItem(item) + rgb = self._get_rgb_by_label(unique_name) + self.uniqLabelList.setItemLabel(item, unique_name, rgb) + self.labelDialog.addLabelHistory(shape.label) + self.IDDialog.addIDHistory(str(shape.track_id)) + for action in self.actions.onShapesPresent: + action.setEnabled(True) + self._update_shape_color(shape) + label_list_item.setText( + '{} '.format( + html.escape(text), *shape.fill_color.getRgb()[:3] + ) + ) + + def _update_shape_color(self, shape): + unique_name = shape.label + '_' + str(shape.track_id) + r, g, b = self._get_rgb_by_label(unique_name) + shape.line_color = QtGui.QColor(r, g, b) + shape.vertex_fill_color = QtGui.QColor(r, g, b) + shape.hvertex_fill_color = QtGui.QColor(255, 255, 255) + shape.fill_color = QtGui.QColor(r, g, b, 128) + shape.select_line_color = QtGui.QColor(255, 255, 255) + shape.select_fill_color = QtGui.QColor(r, g, b, 155) + + def _get_rgb_by_label(self, label): + if self._config["shape_color"] == "auto": + item = self.uniqLabelList.findItemByLabel(label) + if item is None: + item = self.uniqLabelList.createItemFromLabel(label) + self.uniqLabelList.addItem(item) + rgb = self._get_rgb_by_label(label) + self.uniqLabelList.setItemLabel(item, label, rgb) + label_id = self.uniqLabelList.indexFromItem(item).row() + 1 + label_id += self._config["shift_auto_shape_color"] + return LABEL_COLORMAP[label_id % len(LABEL_COLORMAP)] + elif ( + self._config["shape_color"] == "manual" + and self._config["label_colors"] + # and label in self._config["label_colors"] + ): + # return self._config["label_colors"][label] + if label.split('_')[-1] == 'None': + return [224, 224, 0] + else: + return self._config["label_colors"][int(label.split('_')[-1])][int(label.split('_')[-1])] + elif self._config["default_shape_color"]: + return self._config["default_shape_color"] + return (0, 255, 0) + + def remLabels(self, shapes): + for shape in shapes: + item = self.labelList.findItemByShape(shape) + self.labelList.removeItem(item) + + def loadShapes(self, shapes, replace=True): + self._noSelectionSlot = True + for shape in shapes: + self.addLabel(shape) + self.labelList.clearSelection() + self.IDList.clearSelection() + self._noSelectionSlot = False + self.canvas.loadShapes(shapes, replace=replace) + + def loadLabels(self, shapes): + s = [] + for shape in shapes: + label = shape["label"] + points = shape["points"] + shape_type = shape["shape_type"] + flags = shape["flags"] + description = shape.get("description", "") + group_id = shape["group_id"] + track_id = shape["track_id"] + + if not points: + # skip point-empty shape + continue + + if self.ir_activated == True and label == self.ir_name and track_id == self.ir_id: + deltas = [ + [self.ir_mod_shape[0][0] - self.ir_old_shape[0][0], self.ir_mod_shape[0][1] - self.ir_old_shape[0][1]], + [self.ir_mod_shape[1][0] - self.ir_old_shape[1][0], self.ir_mod_shape[1][1] - self.ir_old_shape[1][1]], + ] + + points = [ + [shape['points'][0][0] + deltas[0][0] ,shape['points'][0][1] + deltas[0][1]], + [shape['points'][1][0] + deltas[1][0] ,shape['points'][1][1] + deltas[1][1]] + ] + # self.ir_mod_shape + # self.ir_old_shape + # load_shape + # import ipdb ;ipdb.set_trace() + + shape = Shape( + label=label, + shape_type=shape_type, + group_id=group_id, + track_id=track_id, + description=description, + mask=shape["mask"], + ) + for x, y in points: + shape.addPoint(QtCore.QPointF(x, y)) + shape.close() + + default_flags = {} + if self._config["label_flags"]: + for pattern, keys in self._config["label_flags"].items(): + if re.match(pattern, label): + for key in keys: + default_flags[key] = False + shape.flags = default_flags + shape.flags.update(flags) + + s.append(shape) + self.loadShapes(s) + + def loadFlags(self, flags): + self.flag_widget.clear() + for key, flag in flags.items(): + item = QtWidgets.QListWidgetItem(key) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Checked if flag else Qt.Unchecked) + self.flag_widget.addItem(item) + + def saveLabels(self, filename): + lf = LabelFile() + + def format_shape(s): + data = s.other_data.copy() + data.update( + dict( + label=s.label.encode("utf-8") if PY2 else s.label, + points=[(p.x(), p.y()) for p in s.points], + group_id=s.group_id, + track_id=s.track_id, + description=s.description, + shape_type=s.shape_type, + flags=s.flags, + mask=None if s.mask is None else utils.img_arr_to_b64(s.mask), + ) + ) + return data + + shapes = [format_shape(item.shape()) for item in self.labelList] + flags = {} + for i in range(self.flag_widget.count()): + item = self.flag_widget.item(i) + key = item.text() + flag = item.checkState() == Qt.Checked + flags[key] = flag + try: + imagePath = osp.relpath(self.imagePath, osp.dirname(filename)) + # imageData = self.imageData if self._config["store_data"] else None + imageData = None + if osp.dirname(filename) and not osp.exists(osp.dirname(filename)): + os.makedirs(osp.dirname(filename)) + lf.save( + filename=filename, + shapes=shapes, + imagePath=imagePath, + imageData=imageData, + imageHeight=self.image.height(), + imageWidth=self.image.width(), + flags=flags, + ) + self.labelFile = lf + items = self.fileListWidget.findItems(self.imagePath, Qt.MatchExactly) + if len(items) > 0: + if len(items) != 1: + raise RuntimeError("There are duplicate files.") + items[0].setCheckState(Qt.Checked) + # disable allows next and previous image to proceed + # self.filename = filename + return True + except LabelFileError as e: + self.errorMessage( + self.tr("Error saving label data"), self.tr("%s") % e + ) + return False + + def duplicateSelectedShape(self): + added_shapes = self.canvas.duplicateSelectedShapes() + for shape in added_shapes: + self.addLabel(shape) + self.setDirty() + + def pasteSelectedShape(self): + self.loadShapes(self._copied_shapes, replace=False) + self.setDirty() + + def copySelectedShape(self): + self._copied_shapes = [s.copy() for s in self.canvas.selectedShapes] + self.actions.paste.setEnabled(len(self._copied_shapes) > 0) + + def labelSelectionChanged(self): + if self._noSelectionSlot: + return + if self.canvas.editing(): + selected_shapes = [] + for item in self.labelList.selectedItems(): + selected_shapes.append(item.shape()) + if selected_shapes: + self.canvas.selectShapes(selected_shapes) + else: + self.canvas.deSelectShape() + + def IDSelectionChanged(self): + if self._noSelectionSlot: + return + if self.canvas.editing(): + selected_shapes = [] + for item in self.IDList.selectedItems(): + selected_shapes.append(item.shape()) + if selected_shapes: + self.canvas.selectShapes(selected_shapes) + else: + self.canvas.deSelectShape() + + def labelItemChanged(self, item): + shape = item.shape() + self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) + + def IDItemChanged(self, item): + shape = item.shape() + self.canvas.setShapeVisible(shape, item.checkState() == Qt.Checked) + + def labelOrderChanged(self): + self.setDirty() + self.canvas.loadShapes([item.shape() for item in self.labelList]) + + def IDOrderChanged(self): + self.setDirty() + self.canvas.loadShapes([item.shape() for item in self.IDList]) + + # Callback functions: + + def newShape(self): + """Pop-up and give focus to the label editor. + + position MUST be in global coordinates. + """ + items = self.uniqLabelList.selectedItems() + text_label = None + text_id = None + if items: + text_label = items[0].data(Qt.UserRole) + flags = {} + group_id = None + description = "" + if self._config["display_label_popup"] or not text_label: + previous_text_label = self.labelDialog.edit.text() + previous_text_id = self.IDDialog.edit.text() + if self.mode == "NORMAL": + text_label, flags, group_id, description = self.labelDialog.popUp(text_label) + text_id = self.IDDialog.popUp(text_id) + else: + text_label = self.label_INPO + flags = {} + group_id = None + description = '' + text_id = self.ID_INPO + if not text_label: + self.labelDialog.edit.setText(previous_text_label) + if not text_id: + self.IDDialog.edit.setText(previous_text_id) + + if text_label and not self.validateLabel(text_label): + self.errorMessage( + self.tr("Invalid label"), + self.tr("Invalid label '{}' with validation type '{}'").format( + text_label, self._config["validate_label"] + ), + ) + text_label = "" + if text_label: + self.labelList.clearSelection() + self.IDList.clearSelection() + shape = self.canvas.setLastLabel(text_label, flags) + shape.group_id = group_id + shape.track_id = text_id + shape.description = description + self.addLabel(shape) + self.actions.editMode.setEnabled(True) + self.actions.undoLastPoint.setEnabled(False) + self.actions.undo.setEnabled(True) + self.setDirty() + else: + self.canvas.undoLastLine() + self.canvas.shapesBackups.pop() + + def scrollRequest(self, delta, orientation): + units = -delta * 0.1 # natural scroll + bar = self.scrollBars[orientation] + value = bar.value() + bar.singleStep() * units + self.setScroll(orientation, value) + + def setScroll(self, orientation, value): + self.scrollBars[orientation].setValue(int(value)) + self.scroll_values[orientation][self.filename] = value + + def setZoom(self, value): + self.actions.fitWidth.setChecked(False) + self.actions.fitWindow.setChecked(False) + self.zoomMode = self.MANUAL_ZOOM + self.zoomWidget.setValue(value) + self.zoom_values[self.filename] = (self.zoomMode, value) + + def addZoom(self, increment=1.1): + zoom_value = self.zoomWidget.value() * increment + if increment > 1: + zoom_value = math.ceil(zoom_value) + else: + zoom_value = math.floor(zoom_value) + self.setZoom(zoom_value) + + def zoomRequest(self, delta, pos): + canvas_width_old = self.canvas.width() + units = 1.1 + if delta < 0: + units = 0.9 + self.addZoom(units) + + canvas_width_new = self.canvas.width() + if canvas_width_old != canvas_width_new: + canvas_scale_factor = canvas_width_new / canvas_width_old + + x_shift = round(pos.x() * canvas_scale_factor) - pos.x() + y_shift = round(pos.y() * canvas_scale_factor) - pos.y() + + self.setScroll( + Qt.Horizontal, + self.scrollBars[Qt.Horizontal].value() + x_shift, + ) + self.setScroll( + Qt.Vertical, + self.scrollBars[Qt.Vertical].value() + y_shift, + ) + + def setFitWindow(self, value=True): + if value: + self.actions.fitWidth.setChecked(False) + self.zoomMode = self.FIT_WINDOW if value else self.MANUAL_ZOOM + self.adjustScale() + + def setFitWidth(self, value=True): + if value: + self.actions.fitWindow.setChecked(False) + self.zoomMode = self.FIT_WIDTH if value else self.MANUAL_ZOOM + self.adjustScale() + + def enableKeepPrevScale(self, enabled): + self._config["keep_prev_scale"] = enabled + self.actions.keepPrevScale.setChecked(enabled) + + def onNewBrightnessContrast(self, qimage): + self.canvas.loadPixmap(QtGui.QPixmap.fromImage(qimage), clear_shapes=False) + + def brightnessContrast(self, value): + dialog = BrightnessContrastDialog( + utils.img_data_to_pil(self.imageData), + self.onNewBrightnessContrast, + parent=self, + ) + brightness, contrast = self.brightnessContrast_values.get( + self.filename, (None, None) + ) + if brightness is not None: + dialog.slider_brightness.setValue(brightness) + if contrast is not None: + dialog.slider_contrast.setValue(contrast) + dialog.exec_() + + brightness = dialog.slider_brightness.value() + contrast = dialog.slider_contrast.value() + self.brightnessContrast_values[self.filename] = (brightness, contrast) + + def togglePolygons(self, value): + flag = value + for item in self.labelList: + if value is None: + flag = item.checkState() == Qt.Unchecked + item.setCheckState(Qt.Checked if flag else Qt.Unchecked) + + def loadFile(self, filename=None): + """Load the specified file, or the last opened file if None.""" + # changing fileListWidget loads file + if filename in self.imageList and ( + self.fileListWidget.currentRow() != self.imageList.index(filename) + ): + self.fileListWidget.setCurrentRow(self.imageList.index(filename)) + self.fileListWidget.repaint() + return + + self.resetState() + self.canvas.setEnabled(False) + if filename is None: # image file name .jpg + filename = self.settings.value("filename", "") + filename = str(filename) + if not QtCore.QFile.exists(filename): + self.errorMessage( + self.tr("Error opening file"), + self.tr("No such file: %s") % filename, + ) + return False + # assumes same name, but json extension + self.status(str(self.tr("Loading %s...")) % osp.basename(str(filename))) + + label_file = osp.splitext(filename)[0] + ".json" + + if os.path.isfile(label_file): + self.ir_old_shapes = [] + for item in LabelFile(label_file).shapes: + self.ir_old_shapes.append(item) + + if self.output_dir: + label_file_without_path = osp.basename(label_file) + label_file = osp.join(self.output_dir, label_file_without_path) + if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): # check if label_file exists and has correct type + try: + self.labelFile = LabelFile(label_file) # FIX LabelFile HERE + except LabelFileError as e: + self.errorMessage( + self.tr("Error opening file"), + self.tr( + "

%s

" + "

Make sure %s is a valid label file." + ) + % (e, label_file), + ) + self.status(self.tr("Error reading %s") % label_file) + return False + self.imageData = self.labelFile.imageData + self.imagePath = osp.join( + osp.dirname(label_file), + self.labelFile.imagePath, + ) + self.otherData = self.labelFile.otherData # dont care + else: + self.imageData = LabelFile.load_image_file(filename) + if self.imageData: + self.imagePath = filename + self.labelFile = None + image = QtGui.QImage.fromData(self.imageData) # load encoded image data + + if image.isNull(): + formats = [ + "*.{}".format(fmt.data().decode()) + for fmt in QtGui.QImageReader.supportedImageFormats() + ] + self.errorMessage( + self.tr("Error opening file"), + self.tr( + "

Make sure {0} is a valid image file.
" + "Supported image formats: {1}

" + ).format(filename, ",".join(formats)), + ) + self.status(self.tr("Error reading %s") % filename) + return False + self.image = image # image data + self.filename = filename + + if self._config["keep_prev"]: + prev_shapes = self.canvas.shapes + self.canvas.loadPixmap(QtGui.QPixmap.fromImage(image)) + flags = {k: False for k in self._config["flags"] or []} + if self.labelFile: # if labelFile exists + self.loadLabels(self.labelFile.shapes) # FIX loadLabels HERE + if self.labelFile.flags is not None: + flags.update(self.labelFile.flags) + self.loadFlags(flags) + if self._config["keep_prev"] and self.noShapes(): # check noShapes() /// Shapes are annotations + self.loadShapes(prev_shapes, replace=False) # load annotation from prev image + self.setDirty() + else: + self.setClean() + self.canvas.setEnabled(True) + # set zoom values + is_initial_load = not self.zoom_values + if self.filename in self.zoom_values: + self.zoomMode = self.zoom_values[self.filename][0] + self.setZoom(self.zoom_values[self.filename][1]) + elif is_initial_load or not self._config["keep_prev_scale"]: + self.adjustScale(initial=True) + # set scroll values + for orientation in self.scroll_values: + if self.filename in self.scroll_values[orientation]: + self.setScroll( + orientation, self.scroll_values[orientation][self.filename] + ) + # set brightness contrast values + dialog = BrightnessContrastDialog( + utils.img_data_to_pil(self.imageData), + self.onNewBrightnessContrast, + parent=self, + ) + brightness, contrast = self.brightnessContrast_values.get( + self.filename, (None, None) + ) + if self._config["keep_prev_brightness"] and self.recentFiles: + brightness, _ = self.brightnessContrast_values.get( + self.recentFiles[0], (None, None) + ) + if self._config["keep_prev_contrast"] and self.recentFiles: + _, contrast = self.brightnessContrast_values.get( + self.recentFiles[0], (None, None) + ) + if brightness is not None: + dialog.slider_brightness.setValue(brightness) + if contrast is not None: + dialog.slider_contrast.setValue(contrast) + self.brightnessContrast_values[self.filename] = (brightness, contrast) + if brightness is not None or contrast is not None: + dialog.onNewValue(None) + self.paintCanvas() + self.addRecentFile(self.filename) + self.toggleActions(True) + self.canvas.setFocus() + self.status(str(self.tr("Loaded %s")) % osp.basename(str(filename))) + return True + + def resizeEvent(self, event): + if ( + self.canvas + and not self.image.isNull() + and self.zoomMode != self.MANUAL_ZOOM + ): + self.adjustScale() + super(MainWindow, self).resizeEvent(event) + + def paintCanvas(self): + assert not self.image.isNull(), "cannot paint null image" + self.canvas.scale = 0.01 * self.zoomWidget.value() + self.canvas.adjustSize() + self.canvas.update() + + def adjustScale(self, initial=False): + value = self.scalers[self.FIT_WINDOW if initial else self.zoomMode]() + value = int(100 * value) + self.zoomWidget.setValue(value) + self.zoom_values[self.filename] = (self.zoomMode, value) + + def scaleFitWindow(self): + """Figure out the size of the pixmap to fit the main widget.""" + e = 2.0 # So that no scrollbars are generated. + w1 = self.centralWidget().width() - e + h1 = self.centralWidget().height() - e + a1 = w1 / h1 + # Calculate a new scale value based on the pixmap's aspect ratio. + w2 = self.canvas.pixmap.width() - 0.0 + h2 = self.canvas.pixmap.height() - 0.0 + a2 = w2 / h2 + return w1 / w2 if a2 >= a1 else h1 / h2 + + def scaleFitWidth(self): + # The epsilon does not seem to work too well here. + w = self.centralWidget().width() - 2.0 + return w / self.canvas.pixmap.width() + + def enableSaveImageWithData(self, enabled): + self._config["store_data"] = enabled + self.actions.saveWithImageData.setChecked(enabled) + + def closeEvent(self, event): + if not self.mayContinue(): + event.ignore() + self.settings.setValue("filename", self.filename if self.filename else "") + self.settings.setValue("window/size", self.size()) + self.settings.setValue("window/position", self.pos()) + self.settings.setValue("window/state", self.saveState()) + self.settings.setValue("recentFiles", self.recentFiles) + # ask the use for where to save the labels + # self.settings.setValue('window/geometry', self.saveGeometry()) + + def dragEnterEvent(self, event): + extensions = [ + ".%s" % fmt.data().decode().lower() + for fmt in QtGui.QImageReader.supportedImageFormats() + ] + if event.mimeData().hasUrls(): + items = [i.toLocalFile() for i in event.mimeData().urls()] + if any([i.lower().endswith(tuple(extensions)) for i in items]): + event.accept() + else: + event.ignore() + + def dropEvent(self, event): + if not self.mayContinue(): + event.ignore() + return + items = [i.toLocalFile() for i in event.mimeData().urls()] + self.importDroppedImageFiles(items) + + # User Dialogs # + + def loadRecent(self, filename): + if self.mayContinue(): + self.loadFile(filename) + + def openPrevImg(self, _value=False): + keep_prev = self._config["keep_prev"] + if QtWidgets.QApplication.keyboardModifiers() == ( + Qt.ControlModifier | Qt.ShiftModifier + ): + self._config["keep_prev"] = True + + if not self.mayContinue(): + return + + if len(self.imageList) <= 0: + return + + if self.mode == "NORMAL" or self.mode == "None": + self.ir_activated = False + if self.filename is None: + return + + currIndex = self.imageList.index(self.filename) + if currIndex - 1 >= 0: + filename = self.imageList[currIndex - 1] + if filename: + self.loadFile(filename) + + self._config["keep_prev"] = keep_prev + else: + currIndex = self.INTERPOLATION_list.index(self.filename) + if currIndex - 1 >= 0: + filename = self.INTERPOLATION_list[currIndex - 1] + if filename: + self.loadFile(filename) + + self._config["keep_prev"] = keep_prev + + def openNextImg(self, _value=False, load=True): + keep_prev = self._config["keep_prev"] + if QtWidgets.QApplication.keyboardModifiers() == ( + Qt.ControlModifier | Qt.ShiftModifier + ): + self._config["keep_prev"] = True + + if not self.mayContinue(): + return + + if len(self.imageList) <= 0: + return + + if self.mode == "NORMAL" or self.mode == "None": + if self.interpolationrefine_list.checkBox.isChecked() and self.ir_name != "None" and self.ir_id != "None": + found = False + # original + for item in self.ir_old_shapes: + if item['label'] == self.ir_name and item['track_id'] == self.ir_id: + self.ir_old_shape = item['points'] + # modified + for item in self.labelList: + if item.shape().label == self.ir_name and item.shape().track_id == self.ir_id: + found = True + self.ir_mod_shape = [[p.x(), p.y()] for p in item.shape().points] + if found == False: + self.ir_old_shape = "None" + self.ir_mod_shape = "None" + self.ir_activated = True + + filename = None + if self.filename is None: + filename = self.imageList[0] + else: + currIndex = self.imageList.index(self.filename) + if currIndex + 1 < len(self.imageList): + filename = self.imageList[currIndex + 1] + else: + filename = self.imageList[-1] + self.filename = filename + + if self.filename and load: + self.loadFile(self.filename) + + self._config["keep_prev"] = keep_prev + else: + currIndex = self.INTERPOLATION_list.index(self.filename) + if currIndex + 1 < len(self.INTERPOLATION_list): + filename = self.INTERPOLATION_list[currIndex + 1] + else: + filename = self.INTERPOLATION_list[-1] + self.filename = filename + + if self.filename and load: + self.loadFile(self.filename) + + self._config["keep_prev"] = keep_prev + + def openFile(self, _value=False): + if not self.mayContinue(): + return + path = osp.dirname(str(self.filename)) if self.filename else "." + formats = [ + "*.{}".format(fmt.data().decode()) + for fmt in QtGui.QImageReader.supportedImageFormats() + ] + filters = self.tr("Image & Label files (%s)") % " ".join( + formats + ["*%s" % LabelFile.suffix] + ) + fileDialog = FileDialogPreview(self) + fileDialog.setFileMode(FileDialogPreview.ExistingFile) + fileDialog.setNameFilter(filters) + fileDialog.setWindowTitle( + self.tr("%s - Choose Image or Label file") % __appname__, + ) + fileDialog.setWindowFilePath(path) + fileDialog.setViewMode(FileDialogPreview.Detail) + if fileDialog.exec_(): + fileName = fileDialog.selectedFiles()[0] + if fileName: + self.loadFile(fileName) + + def changeOutputDirDialog(self, _value=False): + default_output_dir = self.output_dir + if default_output_dir is None and self.filename: + default_output_dir = osp.dirname(self.filename) + if default_output_dir is None: + default_output_dir = self.currentPath() + + output_dir = QtWidgets.QFileDialog.getExistingDirectory( + self, + self.tr("%s - Save/Load Annotations in Directory") % __appname__, + default_output_dir, + QtWidgets.QFileDialog.ShowDirsOnly + | QtWidgets.QFileDialog.DontResolveSymlinks, + ) + output_dir = str(output_dir) + + if not output_dir: + return + + self.output_dir = output_dir + + self.statusBar().showMessage( + self.tr("%s . Annotations will be saved/loaded in %s") + % ("Change Annotations Dir", self.output_dir) + ) + self.statusBar().show() + + current_filename = self.filename + self.importDirImages(self.lastOpenDir, load=False) + + if current_filename in self.imageList: + # retain currently selected file + self.fileListWidget.setCurrentRow(self.imageList.index(current_filename)) + self.fileListWidget.repaint() + + def saveFile(self, _value=False): + assert not self.image.isNull(), "cannot save empty image" + if self.labelFile: + # DL20180323 - overwrite when in directory + self._saveFile(self.labelFile.filename) + elif self.output_file: + self._saveFile(self.output_file) + self.close() + else: + self._saveFile(self.saveFileDialog()) + + def saveFileAs(self, _value=False): + assert not self.image.isNull(), "cannot save empty image" + self._saveFile(self.saveFileDialog()) + + def saveFileDialog(self): + caption = self.tr("%s - Choose File") % __appname__ + filters = self.tr("Label files (*%s)") % LabelFile.suffix + if self.output_dir: + dlg = QtWidgets.QFileDialog(self, caption, self.output_dir, filters) + else: + dlg = QtWidgets.QFileDialog(self, caption, self.currentPath(), filters) + dlg.setDefaultSuffix(LabelFile.suffix[1:]) + dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) + dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False) + dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False) + basename = osp.basename(osp.splitext(self.filename)[0]) + if self.output_dir: + default_labelfile_name = osp.join( + self.output_dir, basename + LabelFile.suffix + ) + else: + default_labelfile_name = osp.join( + self.currentPath(), basename + LabelFile.suffix + ) + filename = dlg.getSaveFileName( + self, + self.tr("Choose File"), + default_labelfile_name, + self.tr("Label files (*%s)") % LabelFile.suffix, + ) + if isinstance(filename, tuple): + filename, _ = filename + return filename + + def _saveFile(self, filename): + if filename and self.saveLabels(filename): + self.addRecentFile(filename) + self.setClean() + + def closeFile(self, _value=False): + if not self.mayContinue(): + return + self.resetState() + self.setClean() + self.toggleActions(False) + self.canvas.setEnabled(False) + self.actions.saveAs.setEnabled(False) + + def getLabelFile(self): + if self.filename.lower().endswith(".json"): + label_file = self.filename + else: + label_file = osp.splitext(self.filename)[0] + ".json" + + return label_file + + def deleteFile(self): + mb = QtWidgets.QMessageBox + msg = self.tr( + "You are about to permanently delete this label file, " "proceed anyway?" + ) + answer = mb.warning(self, self.tr("Attention"), msg, mb.Yes | mb.No) + if answer != mb.Yes: + return + + label_file = self.getLabelFile() + if osp.exists(label_file): + os.remove(label_file) + logger.info("Label file is removed: {}".format(label_file)) + + item = self.fileListWidget.currentItem() + item.setCheckState(Qt.Unchecked) + + self.resetState() + + # Message Dialogs. # + def hasLabels(self): + if self.noShapes(): + self.errorMessage( + "No objects labeled", + "You must label at least one object to save the file.", + ) + return False + return True + + def hasLabelFile(self): + if self.filename is None: + return False + + label_file = self.getLabelFile() + return osp.exists(label_file) + + def mayContinue(self): + if not self.dirty: + return True + mb = QtWidgets.QMessageBox + msg = self.tr('Save annotations to "{}" before closing?').format(self.filename) + answer = mb.question( + self, + self.tr("Save annotations?"), + msg, + mb.Save | mb.Discard | mb.Cancel, + mb.Save, + ) + if answer == mb.Discard: + return True + elif answer == mb.Save: + self.saveFile() + return True + else: # answer == mb.Cancel + return False + + def errorMessage(self, title, message): + return QtWidgets.QMessageBox.critical( + self, title, "

%s

%s" % (title, message) + ) + + def informationMessage(self, title, message): + return QtWidgets.QMessageBox.information( + self, title, "

%s

%s" % (title, message) + ) + + def currentPath(self): + return osp.dirname(str(self.filename)) if self.filename else "." + + def toggleKeepPrevMode(self): + self._config["keep_prev"] = not self._config["keep_prev"] + + def removeSelectedPoint(self): + self.canvas.removeSelectedPoint() + self.canvas.update() + if not self.canvas.hShape.points: + self.canvas.deleteShape(self.canvas.hShape) + self.remLabels([self.canvas.hShape]) + if self.noShapes(): + for action in self.actions.onShapesPresent: + action.setEnabled(False) + self.setDirty() + + def deleteSelectedShape(self): + # yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No + # msg = self.tr( + # "You are about to permanently delete {} polygons, " "proceed anyway?" + # ).format(len(self.canvas.selectedShapes)) + # if yes == QtWidgets.QMessageBox.warning( + # self, self.tr("Attention"), msg, yes | no, yes + # ): + # self.remLabels(self.canvas.deleteSelected()) + # self.setDirty() + # if self.noShapes(): + # for action in self.actions.onShapesPresent: + # action.setEnabled(False) + + self.remLabels(self.canvas.deleteSelected()) + self.setDirty() + if self.noShapes(): + for action in self.actions.onShapesPresent: + action.setEnabled(False) + + def copyShape(self): + self.canvas.endMove(copy=True) + for shape in self.canvas.selectedShapes: + self.addLabel(shape) + self.labelList.clearSelection() + self.IDList.clearSelection() + self.setDirty() + + def moveShape(self): + self.canvas.endMove(copy=False) + self.setDirty() + + def openDirDialog(self, _value=False, dirpath=None): + if not self.mayContinue(): + return + + defaultOpenDirPath = dirpath if dirpath else "." + if self.lastOpenDir and osp.exists(self.lastOpenDir): + defaultOpenDirPath = self.lastOpenDir + else: + defaultOpenDirPath = osp.dirname(self.filename) if self.filename else "." + + targetDirPath = str( + QtWidgets.QFileDialog.getExistingDirectory( + self, + self.tr("%s - Open Directory") % __appname__, + defaultOpenDirPath, + QtWidgets.QFileDialog.ShowDirsOnly + | QtWidgets.QFileDialog.DontResolveSymlinks, + ) + ) + self.importDirImages(targetDirPath) + + @property + def imageList(self): + lst = [] + for i in range(self.fileListWidget.count()): + item = self.fileListWidget.item(i) + lst.append(item.text()) + return lst + + def importDroppedImageFiles(self, imageFiles): + extensions = [ + ".%s" % fmt.data().decode().lower() + for fmt in QtGui.QImageReader.supportedImageFormats() + ] + + self.filename = None + for file in imageFiles: + if file in self.imageList or not file.lower().endswith(tuple(extensions)): + continue + label_file = osp.splitext(file)[0] + ".json" + if self.output_dir: + label_file_without_path = osp.basename(label_file) + label_file = osp.join(self.output_dir, label_file_without_path) + item = QtWidgets.QListWidgetItem(file) + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + self.fileListWidget.addItem(item) + + if len(self.imageList) > 1: + self.actions.openNextImg.setEnabled(True) + self.actions.openPrevImg.setEnabled(True) + + self.openNextImg() + + def importDirImages(self, dirpath, pattern=None, load=True): + self.actions.openNextImg.setEnabled(True) + self.actions.openPrevImg.setEnabled(True) + + if not self.mayContinue() or not dirpath: + return + + self.lastOpenDir = dirpath + self.filename = None + self.fileListWidget.clear() + + filenames = self.scanAllImages(dirpath) + if pattern: + try: + filenames = [f for f in filenames if re.search(pattern, f)] + except re.error: + pass + for filename in filenames: + label_file = osp.splitext(filename)[0] + ".json" + if self.output_dir: + label_file_without_path = osp.basename(label_file) + label_file = osp.join(self.output_dir, label_file_without_path) + item = QtWidgets.QListWidgetItem(filename) + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + if QtCore.QFile.exists(label_file) and LabelFile.is_label_file(label_file): + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + self.fileListWidget.addItem(item) + self.openNextImg(load=load) + + def scanAllImages(self, folderPath): + extensions = [ + ".%s" % fmt.data().decode().lower() + for fmt in QtGui.QImageReader.supportedImageFormats() + ] + + images = [] + for root, dirs, files in os.walk(folderPath): + for file in files: + if file.lower().endswith(tuple(extensions)): + relativePath = os.path.normpath(osp.join(root, file)) + images.append(relativePath) + images = natsort.os_sorted(images) + return images \ No newline at end of file diff --git a/labelme/cli/__init__.py b/labelme/cli/__init__.py new file mode 100644 index 0000000..75751a5 --- /dev/null +++ b/labelme/cli/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa + +from . import draw_json +from . import draw_label_png +from . import export_json +from . import on_docker diff --git a/labelme/cli/draw_json.py b/labelme/cli/draw_json.py new file mode 100644 index 0000000..0ad3389 --- /dev/null +++ b/labelme/cli/draw_json.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import argparse +import sys + +import imgviz +import matplotlib.pyplot as plt + +from labelme import utils +from labelme.label_file import LabelFile + +PY2 = sys.version_info[0] == 2 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("json_file") + args = parser.parse_args() + + label_file = LabelFile(args.json_file) + img = utils.img_data_to_arr(label_file.imageData) + + label_name_to_value = {"_background_": 0} + for shape in sorted(label_file.shapes, key=lambda x: x["label"]): + label_name = shape["label"] + if label_name in label_name_to_value: + label_value = label_name_to_value[label_name] + else: + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + lbl, _ = utils.shapes_to_label(img.shape, label_file.shapes, label_name_to_value) + + label_names = [None] * (max(label_name_to_value.values()) + 1) + for name, value in label_name_to_value.items(): + label_names[value] = name + lbl_viz = imgviz.label2rgb( + lbl, + imgviz.asgray(img), + label_names=label_names, + font_size=30, + loc="rb", + ) + + plt.subplot(121) + plt.imshow(img) + plt.subplot(122) + plt.imshow(lbl_viz) + plt.show() + + +if __name__ == "__main__": + main() diff --git a/labelme/cli/draw_label_png.py b/labelme/cli/draw_label_png.py new file mode 100644 index 0000000..6fe080a --- /dev/null +++ b/labelme/cli/draw_label_png.py @@ -0,0 +1,86 @@ +import argparse +import os + +import imgviz +import matplotlib.pyplot as plt +import numpy as np + +from labelme.logger import logger + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("label_png", help="label PNG file") + parser.add_argument( + "--labels", + help="labels list (comma separated text or file)", + default=None, + ) + parser.add_argument("--image", help="image file", default=None) + args = parser.parse_args() + + if args.labels is not None: + if os.path.exists(args.labels): + with open(args.labels) as f: + label_names = [label.strip() for label in f] + else: + label_names = args.labels.split(",") + else: + label_names = None + + if args.image is not None: + image = imgviz.io.imread(args.image) + else: + image = None + + label = imgviz.io.imread(args.label_png) + label = label.astype(np.int32) + label[label == 255] = -1 + + unique_label_values = np.unique(label) + + logger.info("Label image shape: {}".format(label.shape)) + logger.info("Label values: {}".format(unique_label_values.tolist())) + if label_names is not None: + logger.info( + "Label names: {}".format( + [ + "{}:{}".format(label_value, label_names[label_value]) + for label_value in unique_label_values + ] + ) + ) + + if args.image: + num_cols = 2 + else: + num_cols = 1 + + plt.figure(figsize=(num_cols * 6, 5)) + + plt.subplot(1, num_cols, 1) + plt.title(args.label_png) + label_viz = imgviz.label2rgb( + label=label, label_names=label_names, font_size=label.shape[1] // 30 + ) + plt.imshow(label_viz) + + if image is not None: + plt.subplot(1, num_cols, 2) + label_viz_with_overlay = imgviz.label2rgb( + label=label, + image=image, + label_names=label_names, + font_size=label.shape[1] // 30, + ) + plt.title("{}\n{}".format(args.label_png, args.image)) + plt.imshow(label_viz_with_overlay) + + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/labelme/cli/export_json.py b/labelme/cli/export_json.py new file mode 100644 index 0000000..4479321 --- /dev/null +++ b/labelme/cli/export_json.py @@ -0,0 +1,70 @@ +import argparse +import base64 +import json +import os +import os.path as osp + +import imgviz +import PIL.Image + +from labelme import utils +from labelme.logger import logger + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("json_file") + parser.add_argument("-o", "--out", default=None) + args = parser.parse_args() + + json_file = args.json_file + + if args.out is None: + out_dir = osp.splitext(osp.basename(json_file))[0] + out_dir = osp.join(osp.dirname(json_file), out_dir) + else: + out_dir = args.out + if not osp.exists(out_dir): + os.mkdir(out_dir) + + data = json.load(open(json_file)) + imageData = data.get("imageData") + + if not imageData: + imagePath = os.path.join(os.path.dirname(json_file), data["imagePath"]) + with open(imagePath, "rb") as f: + imageData = f.read() + imageData = base64.b64encode(imageData).decode("utf-8") + img = utils.img_b64_to_arr(imageData) + + label_name_to_value = {"_background_": 0} + for shape in sorted(data["shapes"], key=lambda x: x["label"]): + label_name = shape["label"] + if label_name in label_name_to_value: + label_value = label_name_to_value[label_name] + else: + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + lbl, _ = utils.shapes_to_label(img.shape, data["shapes"], label_name_to_value) + + label_names = [None] * (max(label_name_to_value.values()) + 1) + for name, value in label_name_to_value.items(): + label_names[value] = name + + lbl_viz = imgviz.label2rgb( + lbl, imgviz.asgray(img), label_names=label_names, loc="rb" + ) + + PIL.Image.fromarray(img).save(osp.join(out_dir, "img.png")) + utils.lblsave(osp.join(out_dir, "label.png"), lbl) + PIL.Image.fromarray(lbl_viz).save(osp.join(out_dir, "label_viz.png")) + + with open(osp.join(out_dir, "label_names.txt"), "w") as f: + for lbl_name in label_names: + f.write(lbl_name + "\n") + + logger.info("Saved to: {}".format(out_dir)) + + +if __name__ == "__main__": + main() diff --git a/labelme/cli/json_to_dataset.py b/labelme/cli/json_to_dataset.py new file mode 100644 index 0000000..ef37492 --- /dev/null +++ b/labelme/cli/json_to_dataset.py @@ -0,0 +1,80 @@ +import argparse +import base64 +import json +import os +import os.path as osp + +import imgviz +import PIL.Image + +from labelme import utils +from labelme.logger import logger + + +def main(): + logger.warning( + "DEPRECATED: This script will be removed in the near future. " + "Please use `labelme_export_json` instead." + ) + logger.warning( + "NOTE: This script is aimed to demonstrate how to convert a JSON file " + "to a single image dataset. so it won't handle multiple JSON files to " + "generate a real-use dataset." + ) + + parser = argparse.ArgumentParser() + parser.add_argument("json_file") + parser.add_argument("-o", "--out", default=None) + args = parser.parse_args() + + json_file = args.json_file + + if args.out is None: + out_dir = osp.basename(json_file).replace(".", "_") + out_dir = osp.join(osp.dirname(json_file), out_dir) + else: + out_dir = args.out + if not osp.exists(out_dir): + os.mkdir(out_dir) + + data = json.load(open(json_file)) + imageData = data.get("imageData") + + if not imageData: + imagePath = os.path.join(os.path.dirname(json_file), data["imagePath"]) + with open(imagePath, "rb") as f: + imageData = f.read() + imageData = base64.b64encode(imageData).decode("utf-8") + img = utils.img_b64_to_arr(imageData) + + label_name_to_value = {"_background_": 0} + for shape in sorted(data["shapes"], key=lambda x: x["label"]): + label_name = shape["label"] + if label_name in label_name_to_value: + label_value = label_name_to_value[label_name] + else: + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + lbl, _ = utils.shapes_to_label(img.shape, data["shapes"], label_name_to_value) + + label_names = [None] * (max(label_name_to_value.values()) + 1) + for name, value in label_name_to_value.items(): + label_names[value] = name + + lbl_viz = imgviz.label2rgb( + lbl, imgviz.asgray(img), label_names=label_names, loc="rb" + ) + + PIL.Image.fromarray(img).save(osp.join(out_dir, "img.png")) + utils.lblsave(osp.join(out_dir, "label.png"), lbl) + PIL.Image.fromarray(lbl_viz).save(osp.join(out_dir, "label_viz.png")) + + with open(osp.join(out_dir, "label_names.txt"), "w") as f: + for lbl_name in label_names: + f.write(lbl_name + "\n") + + logger.info("Saved to: {}".format(out_dir)) + + +if __name__ == "__main__": + main() diff --git a/labelme/cli/on_docker.py b/labelme/cli/on_docker.py new file mode 100644 index 0000000..a6ea05a --- /dev/null +++ b/labelme/cli/on_docker.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import argparse +import distutils.spawn +import json +import os +import os.path as osp +import platform +import shlex +import subprocess +import sys + + +def get_ip(): + dist = platform.platform().split("-")[0] + if dist == "Linux": + return "" + elif dist == "Darwin": + cmd = "ifconfig en0" + output = subprocess.check_output(shlex.split(cmd)) + if str != bytes: # Python3 + output = output.decode("utf-8") + for row in output.splitlines(): + cols = row.strip().split(" ") + if cols[0] == "inet": + ip = cols[1] + return ip + else: + raise RuntimeError("No ip is found.") + else: + raise RuntimeError("Unsupported platform.") + + +def labelme_on_docker(in_file, out_file): + ip = get_ip() + cmd = "xhost + %s" % ip + subprocess.check_output(shlex.split(cmd)) + + if out_file: + out_file = osp.abspath(out_file) + if osp.exists(out_file): + raise RuntimeError("File exists: %s" % out_file) + else: + open(osp.abspath(out_file), "w") + + cmd = ( + "docker run -it --rm" + " -e DISPLAY={0}:0" + " -e QT_X11_NO_MITSHM=1" + " -v /tmp/.X11-unix:/tmp/.X11-unix" + " -v {1}:{2}" + " -w /home/developer" + ) + in_file_a = osp.abspath(in_file) + in_file_b = osp.join("/home/developer", osp.basename(in_file)) + cmd = cmd.format( + ip, + in_file_a, + in_file_b, + ) + if out_file: + out_file_a = osp.abspath(out_file) + out_file_b = osp.join("/home/developer", osp.basename(out_file)) + cmd += " -v {0}:{1}".format(out_file_a, out_file_b) + cmd += " wkentaro/labelme labelme {0}".format(in_file_b) + if out_file: + cmd += " -O {0}".format(out_file_b) + subprocess.call(shlex.split(cmd)) + + if out_file: + try: + json.load(open(out_file)) + return out_file + except Exception: + if open(out_file).read() == "": + os.remove(out_file) + raise RuntimeError("Annotation is cancelled.") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("in_file", help="Input file or directory.") + parser.add_argument("-O", "--output") + args = parser.parse_args() + + if not distutils.spawn.find_executable("docker"): + print("Please install docker", file=sys.stderr) + sys.exit(1) + + try: + out_file = labelme_on_docker(args.in_file, args.output) + if out_file: + print("Saved to: %s" % out_file) + except RuntimeError as e: + sys.stderr.write(e.__str__() + "\n") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/labelme/config/__init__.py b/labelme/config/__init__.py new file mode 100644 index 0000000..112a919 --- /dev/null +++ b/labelme/config/__init__.py @@ -0,0 +1,75 @@ +import os.path as osp +import shutil + +import yaml + +from labelme.logger import logger + +here = osp.dirname(osp.abspath(__file__)) + + +def update_dict(target_dict, new_dict, validate_item=None): + for key, value in new_dict.items(): + if validate_item: + validate_item(key, value) + if key not in target_dict: + logger.warn("Skipping unexpected key in config: {}".format(key)) + continue + if isinstance(target_dict[key], dict) and isinstance(value, dict): + update_dict(target_dict[key], value, validate_item=validate_item) + else: + target_dict[key] = value + + +# ----------------------------------------------------------------------------- + + +def get_default_config(): + config_file = osp.join(here, "default_config.yaml") + with open(config_file) as f: + config = yaml.safe_load(f) + + # save default config to ~/.labelmerc + user_config_file = osp.join(osp.expanduser("~"), ".labelmerc") + if not osp.exists(user_config_file): + try: + shutil.copy(config_file, user_config_file) + except Exception: + logger.warn("Failed to save config: {}".format(user_config_file)) + + return config + + +def validate_config_item(key, value): + if key == "validate_label" and value not in [None, "exact"]: + raise ValueError( + "Unexpected value for config key 'validate_label': {}".format(value) + ) + if key == "shape_color" and value not in [None, "auto", "manual"]: + raise ValueError( + "Unexpected value for config key 'shape_color': {}".format(value) + ) + if key == "labels" and value is not None and len(value) != len(set(value)): + raise ValueError( + "Duplicates are detected for config key 'labels': {}".format(value) + ) + + +def get_config(config_file_or_yaml=None, config_from_args=None): + # 1. default config + config = get_default_config() + + # 2. specified as file or yaml + if config_file_or_yaml is not None: + config_from_yaml = yaml.safe_load(config_file_or_yaml) + if not isinstance(config_from_yaml, dict): + with open(config_from_yaml) as f: + logger.info("Loading config file from: {}".format(config_from_yaml)) + config_from_yaml = yaml.safe_load(f) + update_dict(config, config_from_yaml, validate_item=validate_config_item) + + # 3. command line argument or specified config file + if config_from_args is not None: + update_dict(config, config_from_args, validate_item=validate_config_item) + + return config diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml new file mode 100644 index 0000000..c44eb58 --- /dev/null +++ b/labelme/config/default_config.yaml @@ -0,0 +1,384 @@ +auto_save: true +display_label_popup: true +store_data: false +keep_prev: false +keep_prev_scale: false +keep_prev_brightness: false +keep_prev_contrast: false +logger_level: info + +flags: null +label_flags: null +labels: null +file_search: null +sort_labels: true +validate_label: null + +default_shape_color: [0, 255, 0] +# shape_color: auto # null, 'auto', 'manual' +shape_color: manual # null, 'auto', 'manual' +shift_auto_shape_color: 0 +# label_colors: null +label_colors: [ + 0: [ 0, 0, 0], + 1: [128, 0, 0], + 2: [ 0, 128, 0], + 3: [128, 128, 0], + 4: [ 0, 0, 128], + 5: [128, 0, 128], + 6: [ 0, 128, 128], + 7: [128, 128, 128], + 8: [128, 64, 128], + 9: [ 64, 64, 0], + 10: [ 64, 128, 0], + 11: [192, 128, 0], + 12: [ 64, 0, 128], + 13: [192, 0, 128], + 14: [ 64, 128, 128], + 15: [192, 128, 128], + 16: [ 0, 64, 0], + 17: [128, 64, 0], + 18: [ 0, 192, 0], + 19: [128, 192, 0], + 20: [ 0, 64, 128], + 21: [ 64, 0, 0], + 22: [ 0, 192, 128], + 23: [128, 192, 128], + 24: [192, 0, 0], + 25: [192, 64, 0], + 26: [ 64, 192, 0], + 27: [192, 192, 0], + 28: [ 64, 64, 128], + 29: [192, 64, 128], + 30: [ 64, 192, 128], + 31: [192, 192, 128], + 32: [ 0, 0, 64], + 33: [128, 0, 64], + 34: [ 0, 128, 64], + 35: [128, 128, 64], + 36: [ 0, 0, 192], + 37: [128, 0, 192], + 38: [ 0, 128, 192], + 39: [128, 128, 192], + 40: [ 64, 0, 64], + 41: [192, 0, 64], + 42: [ 64, 128, 64], + 43: [192, 128, 64], + 44: [ 64, 0, 192], + 45: [192, 0, 192], + 46: [ 64, 128, 192], + 47: [192, 128, 192], + 48: [ 0, 64, 64], + 49: [128, 64, 64], + 50: [ 0, 192, 64], + 51: [128, 192, 64], + 52: [ 0, 64, 192], + 53: [128, 64, 192], + 54: [ 0, 192, 192], + 55: [128, 192, 192], + 56: [ 64, 64, 64], + 57: [192, 64, 64], + 58: [ 64, 192, 64], + 59: [192, 192, 64], + 60: [ 64, 64, 192], + 61: [192, 64, 192], + 62: [ 64, 192, 192], + 63: [192, 192, 192], + 64: [ 32, 0, 0], + 65: [160, 0, 0], + 66: [ 32, 128, 0], + 67: [160, 128, 0], + 68: [ 32, 0, 128], + 69: [160, 0, 128], + 70: [ 32, 128, 128], + 71: [160, 128, 128], + 72: [ 96, 0, 0], + 73: [224, 0, 0], + 74: [ 96, 128, 0], + 75: [224, 128, 0], + 76: [ 96, 0, 128], + 77: [224, 0, 128], + 78: [ 96, 128, 128], + 79: [224, 128, 128], + 80: [ 32, 64, 0], + 81: [160, 64, 0], + 82: [ 32, 192, 0], + 83: [160, 192, 0], + 84: [ 32, 64, 128], + 85: [160, 64, 128], + 86: [ 32, 192, 128], + 87: [160, 192, 128], + 88: [ 96, 64, 0], + 89: [224, 64, 0], + 90: [ 96, 192, 0], + 91: [224, 192, 0], + 92: [ 96, 64, 128], + 93: [224, 64, 128], + 94: [ 96, 192, 128], + 95: [224, 192, 128], + 96: [ 32, 0, 64], + 97: [160, 0, 64], + 98: [ 32, 128, 64], + 99: [160, 128, 64], + 100: [ 32, 0, 192], + 101: [160, 0, 192], + 102: [ 32, 128, 192], + 103: [160, 128, 192], + 104: [ 96, 0, 64], + 105: [224, 0, 64], + 106: [ 96, 128, 64], + 107: [224, 128, 64], + 108: [ 96, 0, 192], + 109: [224, 0, 192], + 110: [ 96, 128, 192], + 111: [224, 128, 192], + 112: [ 32, 64, 64], + 113: [160, 64, 64], + 114: [ 32, 192, 64], + 115: [160, 192, 64], + 116: [ 32, 64, 192], + 117: [160, 64, 192], + 118: [ 32, 192, 192], + 119: [160, 192, 192], + 120: [ 96, 64, 64], + 121: [224, 64, 64], + 122: [ 96, 192, 64], + 123: [224, 192, 64], + 124: [ 96, 64, 192], + 125: [224, 64, 192], + 126: [ 96, 192, 192], + 127: [224, 192, 192], + 128: [ 0, 32, 0], + 129: [128, 32, 0], + 130: [ 0, 160, 0], + 131: [128, 160, 0], + 132: [ 0, 32, 128], + 133: [128, 32, 128], + 134: [ 0, 160, 128], + 135: [128, 160, 128], + 136: [ 64, 32, 0], + 137: [192, 32, 0], + 138: [ 64, 160, 0], + 139: [192, 160, 0], + 140: [ 64, 32, 128], + 141: [192, 32, 128], + 142: [ 64, 160, 128], + 143: [192, 160, 128], + 144: [ 0, 96, 0], + 145: [128, 96, 0], + 146: [ 0, 224, 0], + 147: [128, 224, 0], + 148: [ 0, 96, 128], + 149: [128, 96, 128], + 150: [ 0, 224, 128], + 151: [128, 224, 128], + 152: [ 64, 96, 0], + 153: [192, 96, 0], + 154: [ 64, 224, 0], + 155: [192, 224, 0], + 156: [ 64, 96, 128], + 157: [192, 96, 128], + 158: [ 64, 224, 128], + 159: [192, 224, 128], + 160: [ 0, 32, 64], + 161: [128, 32, 64], + 162: [ 0, 160, 64], + 163: [128, 160, 64], + 164: [ 0, 32, 192], + 165: [128, 32, 192], + 166: [ 0, 160, 192], + 167: [128, 160, 192], + 168: [ 64, 32, 64], + 169: [192, 32, 64], + 170: [ 64, 160, 64], + 171: [192, 160, 64], + 172: [ 64, 32, 192], + 173: [192, 32, 192], + 174: [ 64, 160, 192], + 175: [192, 160, 192], + 176: [ 0, 96, 64], + 177: [128, 96, 64], + 178: [ 0, 224, 64], + 179: [128, 224, 64], + 180: [ 0, 96, 192], + 181: [128, 96, 192], + 182: [ 0, 224, 192], + 183: [128, 224, 192], + 184: [ 64, 96, 64], + 185: [192, 96, 64], + 186: [ 64, 224, 64], + 187: [192, 224, 64], + 188: [ 64, 96, 192], + 189: [192, 96, 192], + 190: [ 64, 224, 192], + 191: [192, 224, 192], + 192: [ 32, 32, 0], + 193: [160, 32, 0], + 194: [ 32, 160, 0], + 195: [160, 160, 0], + 196: [ 32, 32, 128], + 197: [160, 32, 128], + 198: [ 32, 160, 128], + 199: [160, 160, 128], + 200: [ 96, 32, 0], + 201: [224, 32, 0], + 202: [ 96, 160, 0], + 203: [224, 160, 0], + 204: [ 96, 32, 128], + 205: [224, 32, 128], + 206: [ 96, 160, 128], + 207: [224, 160, 128], + 208: [ 32, 96, 0], + 209: [160, 96, 0], + 210: [ 32, 224, 0], + 211: [160, 224, 0], + 212: [ 32, 96, 128], + 213: [160, 96, 128], + 214: [ 32, 224, 128], + 215: [160, 224, 128], + 216: [ 96, 96, 0], + 217: [224, 96, 0], + 218: [ 96, 224, 0], + 219: [224, 224, 0], + 220: [ 96, 96, 128], + 221: [224, 96, 128], + 222: [ 96, 224, 128], + 223: [224, 224, 128], + 224: [ 32, 32, 64], + 225: [160, 32, 64], + 226: [ 32, 160, 64], + 227: [160, 160, 64], + 228: [ 32, 32, 192], + 229: [160, 32, 192], + 230: [ 32, 160, 192], + 231: [160, 160, 192], + 232: [ 96, 32, 64], + 233: [224, 32, 64], + 234: [ 96, 160, 64], + 235: [224, 160, 64], + 236: [ 96, 32, 192], + 237: [224, 32, 192], + 238: [ 96, 160, 192], + 239: [224, 160, 192], + 240: [ 32, 96, 64], + 241: [160, 96, 64], + 242: [ 32, 224, 64], + 243: [160, 224, 64], + 244: [ 32, 96, 192], + 245: [160, 96, 192], + 246: [ 32, 224, 192], + 247: [160, 224, 192], + 248: [ 96, 96, 64], + 249: [224, 96, 64], + 250: [ 96, 224, 64], + 251: [224, 224, 64], + 252: [ 96, 96, 192], + 253: [224, 96, 192], + 254: [ 96, 224, 192], + 'None': [224, 224, 192] +] + +shape: + # drawing + line_color: [0, 255, 0, 128] + fill_color: [0, 0, 0, 64] + vertex_fill_color: [0, 255, 0, 255] + # selecting / hovering + select_line_color: [255, 255, 255, 255] + select_fill_color: [0, 255, 0, 64] + hvertex_fill_color: [255, 255, 255, 255] + point_size: 8 + +ai: + default: 'EfficientSam (accuracy)' + +# main +flag_dock: + show: true + closable: true + movable: true + floatable: true +label_dock: + show: true + closable: true + movable: true + floatable: true +shape_dock: + show: true + closable: true + movable: true + floatable: true +file_dock: + show: true + closable: true + movable: true + floatable: true + +# label_dialog +show_label_text_field: true +label_completion: startswith +fit_to_content: + column: true + row: false + +# canvas +epsilon: 10.0 +canvas: + fill_drawing: true + # None: do nothing + # close: close polygon + double_click: close + # The max number of edits we can undo + num_backups: 10 + # show crosshair + crosshair: + polygon: false + rectangle: true + circle: false + line: false + point: false + linestrip: false + ai_polygon: false + ai_mask: false + +shortcuts: + close: Ctrl+W + open: Ctrl+O + open_dir: Ctrl+U + quit: Ctrl+Q + save: Ctrl+S + save_as: Ctrl+Shift+S + save_to: null + delete_file: Ctrl+Delete + + open_next: [D, Ctrl+Shift+D] + open_prev: [A, Ctrl+Shift+A] + + zoom_in: [Ctrl++, Ctrl+=] + zoom_out: Ctrl+- + zoom_to_original: Ctrl+0 + fit_window: Ctrl+F + fit_width: Ctrl+Shift+F + + create_polygon: Ctrl+N + create_rectangle: Ctrl+R + create_circle: null + create_line: null + create_point: null + create_linestrip: null + edit_polygon: Ctrl+J + delete_polygon: Delete + duplicate_polygon: Ctrl+D + copy_polygon: Ctrl+C + paste_polygon: Ctrl+V + undo: Ctrl+Z + undo_last_point: Ctrl+Z + add_point_to_edge: Ctrl+Shift+P + edit_label: Ctrl+E + edit_id: Ctrl+K + toggle_keep_prev_mode: Ctrl+P + remove_selected_point: [Meta+H, Backspace] + + show_all_polygons: null + hide_all_polygons: null + toggle_all_polygons: T diff --git a/labelme/icons/cancel.png b/labelme/icons/cancel.png new file mode 100644 index 0000000..8fbfab8 Binary files /dev/null and b/labelme/icons/cancel.png differ diff --git a/labelme/icons/close.png b/labelme/icons/close.png new file mode 100644 index 0000000..aa52a8d Binary files /dev/null and b/labelme/icons/close.png differ diff --git a/labelme/icons/color-line.png b/labelme/icons/color-line.png new file mode 100644 index 0000000..6ef10bf Binary files /dev/null and b/labelme/icons/color-line.png differ diff --git a/labelme/icons/color.png b/labelme/icons/color.png new file mode 100644 index 0000000..2ac7184 Binary files /dev/null and b/labelme/icons/color.png differ diff --git a/labelme/icons/copy.png b/labelme/icons/copy.png new file mode 100644 index 0000000..0df8ab1 Binary files /dev/null and b/labelme/icons/copy.png differ diff --git a/labelme/icons/delete.png b/labelme/icons/delete.png new file mode 100644 index 0000000..c355eeb Binary files /dev/null and b/labelme/icons/delete.png differ diff --git a/labelme/icons/done.png b/labelme/icons/done.png new file mode 100644 index 0000000..d8a03f4 Binary files /dev/null and b/labelme/icons/done.png differ diff --git a/labelme/icons/done.svg b/labelme/icons/done.svg new file mode 100644 index 0000000..aa8fd28 --- /dev/null +++ b/labelme/icons/done.svg @@ -0,0 +1,400 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2003-12-22T22:34:35+02:00 + +2004-04-17T21:25:50Z + +Adobe Illustrator 10.0 + +2004-01-19T17:51:02+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq +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 + + + + + + +uuid:4b4d592f-95b8-4bcd-a892-74a536c5e52f + + + +image/svg+xml + + + +test.ai + + + + + + end='w' + + + + + + + + + + diff --git a/labelme/icons/edit.png b/labelme/icons/edit.png new file mode 100644 index 0000000..4b5213e Binary files /dev/null and b/labelme/icons/edit.png differ diff --git a/labelme/icons/expert.png b/labelme/icons/expert.png new file mode 100644 index 0000000..82793d2 Binary files /dev/null and b/labelme/icons/expert.png differ diff --git a/labelme/icons/eye.png b/labelme/icons/eye.png new file mode 100644 index 0000000..c4b6550 Binary files /dev/null and b/labelme/icons/eye.png differ diff --git a/labelme/icons/feBlend-icon.png b/labelme/icons/feBlend-icon.png new file mode 100644 index 0000000..1c1aca8 Binary files /dev/null and b/labelme/icons/feBlend-icon.png differ diff --git a/labelme/icons/file.png b/labelme/icons/file.png new file mode 100644 index 0000000..1ec0515 Binary files /dev/null and b/labelme/icons/file.png differ diff --git a/labelme/icons/fit-width.png b/labelme/icons/fit-width.png new file mode 100644 index 0000000..117e6a0 Binary files /dev/null and b/labelme/icons/fit-width.png differ diff --git a/labelme/icons/fit-window.png b/labelme/icons/fit-window.png new file mode 100644 index 0000000..1992556 Binary files /dev/null and b/labelme/icons/fit-window.png differ diff --git a/labelme/icons/fit.png b/labelme/icons/fit.png new file mode 100644 index 0000000..9e0e817 Binary files /dev/null and b/labelme/icons/fit.png differ diff --git a/labelme/icons/help.png b/labelme/icons/help.png new file mode 100644 index 0000000..93bf094 Binary files /dev/null and b/labelme/icons/help.png differ diff --git a/labelme/icons/icon.icns b/labelme/icons/icon.icns new file mode 100644 index 0000000..ff83787 Binary files /dev/null and b/labelme/icons/icon.icns differ diff --git a/labelme/icons/icon.ico b/labelme/icons/icon.ico new file mode 100644 index 0000000..82cbe80 Binary files /dev/null and b/labelme/icons/icon.ico differ diff --git a/labelme/icons/icon.png b/labelme/icons/icon.png new file mode 100644 index 0000000..7520dd6 Binary files /dev/null and b/labelme/icons/icon.png differ diff --git a/labelme/icons/labels.png b/labelme/icons/labels.png new file mode 100644 index 0000000..c82ffb7 Binary files /dev/null and b/labelme/icons/labels.png differ diff --git a/labelme/icons/labels.svg b/labelme/icons/labels.svg new file mode 100644 index 0000000..652cef3 --- /dev/null +++ b/labelme/icons/labels.svg @@ -0,0 +1,819 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + + +Adobe PDF library 5.00 + + + + + +2004-01-26T11:58:28+02:00 + +2004-03-28T20:41:40Z + +Adobe Illustrator 10.0 + +2004-02-16T23:58:32+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 +f//Z + + + + + + +uuid:4ee3f24b-6ed2-4a2e-8f7a-50b762c8da8b + + + +image/svg+xml + + + +mime.ai + + + + image/svg+xml + end='w' + + +Labels + \ No newline at end of file diff --git a/labelme/icons/new.png b/labelme/icons/new.png new file mode 100644 index 0000000..dd795cf Binary files /dev/null and b/labelme/icons/new.png differ diff --git a/labelme/icons/next.png b/labelme/icons/next.png new file mode 100644 index 0000000..3ea88c4 Binary files /dev/null and b/labelme/icons/next.png differ diff --git a/labelme/icons/objects.png b/labelme/icons/objects.png new file mode 100644 index 0000000..1c7606a Binary files /dev/null and b/labelme/icons/objects.png differ diff --git a/labelme/icons/open.png b/labelme/icons/open.png new file mode 100644 index 0000000..45fa288 Binary files /dev/null and b/labelme/icons/open.png differ diff --git a/labelme/icons/open.svg b/labelme/icons/open.svg new file mode 100644 index 0000000..48e7a34 --- /dev/null +++ b/labelme/icons/open.svg @@ -0,0 +1,577 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/labelme/icons/prev.png b/labelme/icons/prev.png new file mode 100644 index 0000000..7ed78fa Binary files /dev/null and b/labelme/icons/prev.png differ diff --git a/labelme/icons/quit.png b/labelme/icons/quit.png new file mode 100644 index 0000000..7445887 Binary files /dev/null and b/labelme/icons/quit.png differ diff --git a/labelme/icons/save-as.png b/labelme/icons/save-as.png new file mode 100644 index 0000000..1b5d900 Binary files /dev/null and b/labelme/icons/save-as.png differ diff --git a/labelme/icons/save-as.svg b/labelme/icons/save-as.svg new file mode 100644 index 0000000..c8441a1 --- /dev/null +++ b/labelme/icons/save-as.svg @@ -0,0 +1,1358 @@ + + + + + + + + + + + + + + + + + + + + +begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-01-26T11:58:28+02:00 + +2004-03-28T20:41:40Z + +Adobe Illustrator 10.0 + +2004-02-16T23:58:32+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmDzFo +3l7TJdT1e5W1tItuTbszHoiKN2Y+AxV4j5g/5ydvTcMnl/SYlgU0Se/LOzDxMcTIF/4M4qk//QzP +nv8A5YNL/wCRVx/2UYq7/oZnz3/ywaX/AMirj/soxV3/AEMz57/5YNL/AORVx/2UYq7/AKGZ89/8 +sGl/8irj/soxV3/QzPnv/lg0v/kVcf8AZRirv+hmfPf/ACwaX/yKuP8AsoxV3/QzPnv/AJYNL/5F +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 +f//Z + + + + + + +uuid:4ee3f24b-6ed2-4a2e-8f7a-50b762c8da8b + + + +image/svg+xml + + + +mime.ai + + + + + + +end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-02-04T02:08:51+02:00 + +2004-03-29T09:20:16Z + +Adobe Illustrator 10.0 + +2004-02-29T14:54:28+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXzd+b/wDzlWum3k+h+QxFc3EJMdzrkoEkKuNiLZPsyU/nb4fAEb50vZ/YXEBPLsP5v62meXue +A3v5mfmprl080vmLVriXdjHBcTIi17rFCVRfoXOghocEBQhH5NJmepUf8Tfmj/1dtb/6SLv/AJqy +f5fD/Nj8gjxPN3+JvzR/6u2t/wDSRd/81Y/l8P8ANj8gviebv8Tfmj/1dtb/AOki7/5qx/L4f5sf +kF8Tzd/ib80f+rtrf/SRd/8ANWP5fD/Nj8gviebv8Tfmj/1dtb/6SLv/AJqx/L4f5sfkF8Tzd/ib +80f+rtrf/SRd/wDNWP5fD/Nj8gviebv8Tfmj/wBXbW/+ki7/AOasfy+H+bH5BfE83f4m/NH/AKu2 +t/8ASRd/81Y/l8P82PyC+J5u/wATfmj/ANXbW/8ApIu/+asfy+H+bH5BfE83f4m/NH/q7a3/ANJF +3/zVj+Xw/wA2PyC+J5u/xN+aP/V21v8A6SLv/mrH8vh/mx+QXxPN3+JvzR/6u2t/9JF3/wA1Y/l8 +P82PyC+J5u/xN+aP/V21v/pIu/8AmrH8vh/mx+QXxPN3+JvzR/6u2t/9JF3/AM1Y/l8P82PyC+J5 +u/xN+aP/AFdtb/6SLv8A5qx/L4f5sfkF8Tzd/ib80f8Aq7a3/wBJF3/zVj+Xw/zY/IL4nm7/ABN+ +aP8A1dtb/wCki7/5qx/L4f5sfkF8Tzd/ib80f+rtrf8A0kXf/NWP5fD/ADY/IL4nm7/E35o/9XbW +/wDpIu/+asfy+H+bH5BfE82j5t/M+Aes2ta3EI/i9U3N2vGnfly2x/LYT/DH5BePzZ15C/5yh/Mb +y7cxRaxcHzDpQIEsF2f9IC9zHc058v8AX5D9ea/VdiYcg9I4JeXL5NkchD688jeefLvnby/DrmhT ++rayEpLE4CywygAtFKtTxYV+RG4qDnH6nTTwT4JjdyIytkGY6XYq7FXYq7FXYq7FXjX/ADlH+YV1 +5W8hppunymHU/MMj2qSqaMltGoNwynxPNE/2WbrsPSDLl4pfTDf49GvJKg+VPy+8lP5ivecqM9rG +4jWFaqZpTvw57cVUULGvcfMdtYFk7Ac3Ua3VHGAI/XLk+jNK/LfSLS0SK4JYqDSGCkUCV3PBVAPX +vtXwzWT7TlfoAA+11f5Xi3mTIo608meV/wBL2lnLbSSLcc/92sB8Kk70IOU5+0s4xSmCPT5NuDRY +pZBEjmyu2/KnydcFgliF4ip5TT/wY5ov5f1f877B+p2/8kaf+b9pVv8AlT3lL/lkT/kdcf1w/wAv +az+d9kf1I/kjTfzftLR/J/yl/wAsif8AI65/rj/L2s/nfZH9S/yRpv5v2lafyg8p/wDLKn/I65/r +h/l3Wfzvsj+pf5J03837S0fyh8p/8sqf8jrn+uP8u6z+d9kf1L/JOm/m/aWj+UXlP/llj/5HXP8A +XH+XdZ/O+yP6l/knTfzftLX/ACqPyn/yzR/8jrn+uH+XNb/O+yP6l/knTd32lr/lUflX/lmj/wCR +1z/XB/Lmt/nfZH9S/wAk6bu+0u/5VD5W/wCWaP8A5HXP9cf5d1n877I/qX+SdN/N+0u/5VB5Y/5Z +ov8Akdc/1x/l3Wfzvsj+pf5J03837S7/AJU/5a/5Zov+R1z/AFx/l3Wfzvsj+pf5J03837S7/lT3 +lv8A5Zov+R1z/XB/L2s/nfZH9S/yRpv5v2l3/KnfLv8AyzRf8jrn+uP8vaz+d9kf1L/JGm/m/aXf +8qc8v/8ALNF/yOuf64/y9rP532R/Uv8AJGm/m/aXf8qb0H/lmh/5HXP9cf5f1n877I/qX+SNN/N+ +0u/5U1oP/LND/wAjrn+uD+X9Z/O+wfqT/JGn/m/aVk/5P6BDBJM1rEVjUswE1xWg8KnH/RBq/wCd +9g/Uv8kaf+b9pYp5i8oeXLOGBoLQo0j8SRJIe3+Uxza9ldq6jNKQnLkO4Ov1/Z2HGAYj7SkreXdK +IoEZD/Mrmo+Vaj8M3I1eR1fgRee/mD+W8NxE91ZIPrhq0UygL6rbt6ctNubfssevy6XwmJjbYjo5 +ml1csUhGRuB+xJP+cfvzGvfJvny1T1T+iNXdLTUbcn4SWNIpPZkduvgTmq7Z0gy4Sf4obj9L0WOV +F93xSJLGsiGqOAyn2O+cK5K7FXYq7FXYq7FXYq+R/wDnM65lbzjoFsT+6i05pEG/2pJ2VvbpGM6/ +2cH7uR/pfocfNzb/ACCs7caXZzBAJPQuJS3fn9ZMXL/gNs2uvkRirvl+h0GffUm+kfx972EnNKyU +LXfzNpZ/4y/8QOOo/wAWn8PvbdN/fRei6SPjl/1R+vOWDvyjyMsQsIwoWkYVWEYULSMKFhGSVrFV +wOBVwOBVwOBK4HFVwOBK4HAq4HAlcDgVQ1I/7jrn/jE36siUh5X5uH+j23tL/DN52F9U/c6vtX6Q +x0nOidEgNZodNmBAP2aE9jzG4+jL9P8AWGrL9JfNGuSmDzPqEsICGK9maNRsF4ykgCnhmRKArhel +08iccT5B+iHk+4afQbcsalBx+8Bv+Ns8wdknWKuxV2KuxV2KuxV8hf8AOZn/ACneif8AbLH/AFES +52Hs7/dS/rfoDj5uaO/IUf7gbI/8ulx/1GnNlr/7v/O/Q6DN/jEv6v6nqxOahksshXzJpv8Az0/4 +gcjqf8Xn8PvbdL/exei6SPjk/wBUfrzlw9AmBGTYrSMKrCMKFpGFVhGFC0jChYRklaxVcDgVcDgV +cDgSuBxVcDgSuBwKuBwJUdRP+4+5/wCMTfqyJSHlvmwf6Lb+0n8M3XYX1S9zq+1fpDwzzXoX1nzD +eT8a82U1/wBgBm1y6fikS6qGfhFJt5T076lomoJSnOSM/dTMzQYuCTj6rJxh4h5k/wCUi1T/AJjJ +/wDk62bM83fab+6j/VH3P0N8jf8AHBj+Y/5NpnlztGQYq7FXYq7FXYq7FXyF/wA5mf8AKd6J/wBs +sf8AURLnYezv91L+t+gOPm5ph+Q4/wCddsj/AMutx/1Gtmx1/wBH+d+h0Gb/ABiX9X9T1InNUl2n +b+Y9P/56f8QOQ1X+Lz+H3t+l/vYvRtJH7yT/AFR+vOWDv0xIySFhGSQtIwqsIwoWkYVWEYULSMKF +hGSVrFVwOBVwOBVwOBK4HFVwOBK4HAqjf/8AHPuf+MTfqyEkh5j5rH+iQ/65/Uc3XYf1y9zre1Pp +DDpbGzkcu8QZ26k50weeMQoXVvDDZyrEgQNQkD5jLMX1BhMbPmrzN/ykmrf8xlx/ydbMp6XTf3cf +6o+5+hnkb/jgx/Mf8m0zy52bIMVdirsVdirsVdir5C/5zM/5TvRP+2WP+oiXOw9nf7qX9b9AcfNz +TL8iR/zrFif+Xa4/6jWzYa76f879Doc/9/L3fqenE5rEL9KFfMNh85P+IHK9X/cT+H3uRpP72L0f +SR+8k/1f45yzv0xIwqtIwoWEZJC0jCqwjChaRhVYRhQtIwoWEZJWsVXA4FXA4FXA4ErgcVXA4EqV +9/vBc/8AGJv1ZCXJIea+ah/ocfsx/wCInNx2H9cvcHW9qfQGIE507z6HvN7dx8v1jLMfNhPk+Z/N +H/KTav8A8xtx/wAnWzJek0/93H+qPufoX5G/44MfzH/JtM8vdmyDFXYq7FXYq7FXYq+Qv+czP+U7 +0T/tlj/qIlzsPZ3+6l/W/QHHzc0z/Isf86nYH/l3uP8AqNbM/W8v879Doc/9/L3fqelk5rkK2j76 +/ZfN/wDiBynWf3Evx1cjSf3oej6UP3r/AOr/ABzl3fpliq0jCq0jChYRkkLSMKrCMKFpGFVhGFC0 +jChYRklaxVcDgVcDgVcDgSuBxVTvP94rn/jE36shPkyDzjzUP9BX5n/iJzbdifXL4Ou7U+gfFhhO +dS86pXG8TD5frycebGXJ8z+av+Un1j/mNuf+TrZkh6TT/wB3H+qPufoV5G/44MfzH/JtM8vdmyDF +XYq7FXYq7FXYq+Qv+czP+U70T/tlj/qIlzsPZ3+6l/W/QHHzc01/I0f86fp5/wCKLj/qNbM7W8v8 +79Dos/8AfH3fqejE5gMEVoe+u2fzf/iByjW/3Evx1cnR/wB4Ho+l/wB4/wAv45y7v0xxV2KrSMKr +SMKFhGSQtIwqsIwoWkYVWEYULSMKFhGSVrFVwOBVwOBVwOBKy6P+h3H/ABib9WQnySHnnmkf6APY +t/xE5texPrPwdf2n9A+LByc6t5xTfcEZIIL5p82f8pTrP/Mdc/8AJ5syRyek0/8Adx9w+5+hPkb/ +AI4MfzH/ACbTPL3ZsgxV2KuxV2KuxV2KvkL/AJzM/wCU70T/ALZY/wCoiXOw9nf7qX9b9AcfNzTf +8jx/zpWnH/im4/6jHzO1n6f0Oi1H98fd+p6ETmE1o3y/vrdr82/4gcxtd/cycrR/3gej6b/eP8v4 +5y7v0wxV2KuxVaRhVaRhQsIySFpGFVhGFC0jCqwjChaRhQsIyStYquBwKuBwKtuT/olx/wAYm/Vk +J8mUXn/mkf7jj/sv+InNp2L/AHh+Dr+0/oHxYGTnWvONDdgMUPmnzb/yletf8x9z/wAnmzIjyelw +f3cfcH6EeRv+ODH8x/ybTPMHZMgxV2KuxV2KuxV2KvkL/nMz/lO9E/7ZY/6iJc7D2d/upf1v0Bx8 +3NOPyRH/ADo2mn/im4/6jHzN1fP4/odHqP70+5n5OYjUmHlzfWrb5t/xA5ia7+5k5Wi/vA9H07+8 +f5fxzmHfo/FXYq7FXYqtIwqtIwoWEZJC0jCqwjChaRhVYRhQtIwoWEZJWsVXA4Fan/3luP8AjE36 +shk5MosD80D/AHGt8m/4gc2XY394fg4Haf0fN56TnXvNLod5VHz/AFYJclD5p83/APKWa3/zH3X/ +ACebMiPIPS4P7uPuD9CPI3/HBj+Y/wCTaZ5g7JkGKuxV2KuxV2KuxV8hf85mf8p3on/bLH/URLnY +ezv91L+t+gOPm5p1+SYp5B0w/wDFVx/1GPmZq/q+P6HR6n+9PuZ0TmM0pr5Y31iD5t/xA5h6/wDu +i5mi/vA9G0/7b/LOYd8jsVdirsVdirsVWkYVWkYULCMkhaRhVYRhQtIwqsIwoWkYULCMkrWKul/3 +mn/4xt+rK8nJMebB/NA/3Fyf6r/8QObHsb+8Pw+9we0/o+bzgnOxeZVLXe4QfP8AUcjPkmPN81ec +f+Uu1z/toXX/ACebL4fSHpcH0R9wfoP5G/44MfzH/JtM8xdkyDFXYq7FXYq7FXYq+Qv+czP+U70T +/tlj/qIlzsPZ3+6l/W/QHHzc08/JUf8AIPNLP/Fdx/1GSZl6r6z7/wBDpNT/AHh9zNicocdOPKu+ +rQ/M/wDEGzB7Q/ui5uh+sPRbEhXappt3zmXfI3mn8w+/FXeon8w+/FWvUj/mH3jFXepH/MPvGKu9 +WP8AnH3jFXepF/Ov3jFVpeP+dfvGG1Wl4/51+8YbQtLJ/Mv3jDa0tJT+ZfvGHiCKWnj/ADL/AMEP +64eILS08f5l/4If1w8QRS0qP5l/4If1w8YWlpUfzL/wS/wBceMIorCn+Uv8AwS/1w8YXhKyai289 +WXeNgPiB3I+eRnIEJiGFeZx/uKm/1H/4gc2PY/8AefL73B7S+j5vNCc7N5dWsN7uMfP/AIichl+k +so83zX5z/wCUw13/ALaF1/yffL8f0j3PS4foj7g/QbyN/wAcGP5j/k2meYuyZBirsVdirsVdirsV +fIX/ADmZ/wAp3on/AGyx/wBREudh7O/3Uv636A4+bmnv5Lj/AJBxpZ/yLj/qMkzK1X1n3/odJqv7 +w+5mZOVOOmvly5jtrwTyAlIzuFpXdSO9Mw9bjM4cI6uVpJiMrLK/8T2H++5fuX/mrNL/ACdk7x+P +g7b85DuLX+JbD/fcv3L/AM1Y/wAnZO8fj4L+ch3Fr/Elj/vuX7l/5qx/k7J3j8fBfzkO4tf4jsf9 +9y/cv/NWP8nZO8fj4L+ch3Fo+YrH/fcv3L/zVj/J2TvH4+C/nIdxW/4hsv5JPuX/AJqx/k7J3j8f +BfzkO4tfp+y/kk+5f+asf5Oyd4/HwX85DuLX6es/5JPuX/mrH+TsnePx8F/OQ7i1+nbP+ST7l/5q +x/k7J3j8fBfzkO4tfpy0/kk+5f64/wAnZO8fj4L+ch3Fr9N2n8kn3L/XH+TsnePx8F/OQ7i0datf +5JPuX+uP8nZO8fj4L+ch3Fb+mLX+R/uH9cf5Oyd4/HwX85DuLX6Xtv5H+4f1x/k7J3j8fBfzkO4t +fpa2/lf7h/XH+TsnePx8F/OQ7i0dVt/5X+4f1x/k7J3j8fBfzkO4tHVLf+V/uH9cf5Oyd4/HwX85 +DuKW6/dxz6XcKgYFY5DvT+Q++bDs7TSx5Bdbkfe4etzicNvN5sTnWPOojTN7+If63/ETleb6Cyhz +fNnnX/lMte/7aN3/AMn3y/H9I9z02H6B7g/QXyN/xwY/mP8Ak2meYuxZBirsVdirsVdirsVfIX/O +Zn/Kd6J/2yx/1ES52Hs7/dS/rfoDj5uaf/kyP+QZ6Uf8m4/6jJMytT/eH8dHS6r6z7mXk5W4rSyy +JXgxWvWhIxMQVEiOTjdXH+/X/wCCOPAO5eM9603Vz/v1/wDgjh4I9y8Z71pu7n/fz/8ABHDwR7kc +Z71pu7r/AH8//BH+uHw49y8cu9aby6/39J/wR/rh8OPcEccu9ab27/3/ACf8E39cPhx7gjjl3rTe +3f8Av+T/AINv64fDj3BfEl3rTfXn+/5P+Db+uHw49wR4ku8rTfXv/LRJ/wAG39cPhR7gviS7ytN/ +e/8ALRJ/wbf1w+FHuCPEl3ladQvv+WiX/g2/rh8KPcEeJLvK06hff8tMv/Bt/XD4Ue4L4ku8rTqN +/wD8tMv/AAbf1w+FDuCPEl3ladRv/wDlpl/4Nv64fBh3D5L4ku8rTqWof8tUv/Bt/XD4MO4fJHiy +7ytOp6h/y1Tf8jG/rh8GHcPkjxZd5aOp6j/y1Tf8jG/rh8GHcPkviy7ypvqN+6lWuZWVhRlLsQQe +xFcIwwHQfJByS7yhScta0Xo++pQj/W/4icq1H0Fnj+p82+d/+Uz1/wD7aN3/AMn3y7F9I9z02H6B +7g/QTyN/xwY/mP8Ak2meZOxZBirsVdirsVdirsVfIX/OZn/Kd6J/2yx/1ES52Hs7/dS/rfoDj5ub +IfybH/ILtJPtcf8AUZLmTqP70/jo6XVfWWVE5FxFpOFVpOFDCLz82fLtrdz2slteGSCRonKpFQlC +VNKyDbbLRjLLgKgfzh8tf8s17/wEX/VXD4ZXwytP5weWv+Wa9/4CL/qrjwFHhlo/m95b/wCWa8/4 +CL/qrh4Cvhlo/m75b/5Zrz/gIv8Aqrh4V8Mrf+Vt+XD/AMe15/wEX/VXCIFHhF3/ACtjy6f+Pa8/ +4CL/AKqZMYijwy1/ytXy8f8Aj3u/+Ai/6qZYNPJHhl3/ACtPy+f+Pe7/AOAj/wCqmTGll5I8Mtf8 +rQ0A/wDHvd/8BH/1UywaKfkjwy7/AJWboR/497r/AICP/qpkx2fPvCOAtf8AKytDP+6Lr/gI/wDq +pkx2bk7x+PgjgLY/MXRT0guf+Bj/AOa8P8nZO8fj4LwFseftIPSG4/4FP+a8f5Pn3j8fBHAUTY+b +dOvbqO2iimWSQkKXVQNhXejHwyGTSSiLNIMSE4JzGYLCcKFpOFCN0PfVYB/rf8QOU6n+7LZi+oPm +7zx/ymvmD/tpXn/J98uxfQPcHpsX0D3B+gfkb/jgx/Mf8m0zzJ2LIMVdirsVdirsVdir5C/5zM/5 +TvRP+2WP+oiXOw9nf7qX9b9AcfNzZF+To/5BVpB9rj/qMlzI1H98fx0dNq/qLJycXDWk4ULScKEq +/IbT7OTVvMty0S/Wm1BoRPQcxHVmKqT0BPXNL25M3EdKd52bEUS9s/RNv/O/3j+maC3Zu/RNv/O/ +3j+mNq79E2/87/eP6Y2rv0Tb/wA7/eP6Y2rv0Tb/AM7/AHj+mNq79E2/87/eP6Y2rv0Tb/zv94/p +jau/RNv/ADv94/pjau/RNv8Azv8AeP6Y2rv0Tb/zv94/pjau/RNv/O/3j+mNq80/PXTbMeUJmaMP +LbyQvBKwBZC8gRqEU6qc6L2YyyjqwAdpA38nA7RiDiJ7nzykeekEvOpz5cSmsWx9z/xE5jak+gsZ +cmeE5qWhaThQtJwqj/L2+sW4/wBf/iDZRq/7s/jq2YfqD5v89f8AKb+Yf+2nef8AUQ+W4foHuD02 +L6R7n6BeRv8Ajgx/Mf8AJtM8zdiyDFXYq7FXYq7FXYq+Qv8AnMz/AJTvRP8Atlj/AKiJc7D2d/up +f1v0Bx83Nkn5Pj/kEujn/mI/6jJcvz/35/HR02r+osjJyThLScKFhOSQgvyCamo+YR46o3/G2aHt +z6o+533Zv0l7pmhdk7FXYq7FXYq7FXYq7FXYq7FXYq8w/PPfytdr7wf8nRm/9m/8bj7pfc4PaP8A +cn4PntI89IJebTXQUpqlufc/8ROY+c+gsZcmZk5rWhaThVaThQmPlrfW7Yf6/wDybbMfWf3R/HVt +wfWHzh58/wCU58xf9tO8/wCoh8twfRH3B6fH9I9z9AfI3/HBj+Y/5NpnmbsGQYq7FXYq7FXYq7FX +yF/zmZ/yneif9ssf9REudh7O/wB1L+t+gOPm5sm/KEf8gh0Y+9x/1GTZdm/vz+OgdPrOZT8nLHAW +E5JC0nCqX/kO9NT8wf8AbUb/AI2zQ9ufVH3O+7N+kvdPUzQ07Jg/5n+a7ny3o9zq0CGY20cREHMx +hvUnEfUA9OVemZmh03jZRC6u/utpz5eCBl3PIv8AoY3V/wDq1j/pKf8A5ozoR7NxP8f2ftdf/KR/ +m/ay/wDLf81dQ826lcW0tsbQWypJyWZpOXJuNKELmu7U7JGliJCXFZ7nJ0ur8UkVVPZvUzR05rvU +xpXepjSu9TGld6mNK71MaV3qY0rzP8625eXrlf8AjB/ydGb32c/xuPul9zg9o/3J+DwdI89FJebT +PRkpqEJ9z+o5RmPpLCXJlJOYLStJwoWE4UJp5V31+1H/ABk/5NtmNrf7o/D727T/AFh84efv+U68 +x/8AbUvf+oh8swf3cfcHp8f0j3P0B8jf8cGP5j/k2meaOwZBirsVdirsVdirsVfIX/OZn/Kd6J/2 +yx/1ES52Hs7/AHUv636A4+bmyf8AKMf8gc0U/wCVcf8AUZNl2b/GD+OgdPrOZTsnLnXrScKrScKE +s/I1qanr3/bTb/jbND22PVH3O/7N+kvb/UzROyeYfny9fJmoj/iu2/6i0zbdiD/CofH/AHJcTW/3 +R+H3vmQDPQ4wefep/kEeOuah/wAYov8Ak5nOe1Eaxw/rH7nZdmfUfc+l/UziXcu9TFXepirvUxV3 +qYq71MVd6mKvOPzhblolwPaH/k5m79nv8aj7j9zgdo/3J+DxdI89BJebTDTEpeRH3P6jlOQ7MZck +/JzFaFhOFC0nCqbeUd/MVoP+Mn/Jpsxdf/cy+H3hu031h84/mB/ynnmT/tqXv/UQ+Waf+7j/AFR9 +z0+P6R7n6AeRv+ODH8x/ybTPNHYMgxV2KuxV2KuxV2KvkL/nMz/lO9E/7ZY/6iJc7D2d/upf1v0B +x83NlP5TD/kC+iH/AC7n/qMmy3L/AIzL8dA6jWcym5OZDrlpOFC0nChKfyUbjqmue+pN/wAbZpO3 +h6of1Xf9m/SXtXqZz9Oyeafnm9fKOoD/AIrt/wDqKXNz2CP8Lh/nf7kuJrv7o/D73zaFz0mMHnre +nfkWeOt33/GKP/k5nMe1kaxQ/rH7nZ9l/Ufc+j/UzhKdy71MaV3qY0rvUxpXepjSu9TGld6mNK8/ +/NduWlzL7Rf8nM3XYH+NR+P3OD2l/cn4PJEjzvSXmkbYpS4Q/wCfTKpnZjLkmpOUtC0nCq0nJITj +ybv5lsx/xk/5NPmH2h/cy+H3hv0394Hzl+YP/KfeZf8Atq3v/US+Waf+7j/VH3PTw+kPv/yN/wAc +GP5j/k2meaOwZBirsVdirsVdirsVfIX/ADmZ/wAp3on/AGyx/wBREudh7O/3Uv636A4+bmyv8qB/ +yBPRD/xZc/8AUZNlmT/GpfjoHUa1MycynWrScKFhOFUn/JxuOqa1/wBtJv8AjbNR7QD1Q/qu+7M+ +kvZfUznKdm83/Ox+XlW/H/Fdv/1Erm69nh/hkP8AO/3JcTXf3J+H3vncLnp8YvOPSvyUHDWL0+Mc +f/E85P2u/uof1j9ztOy/qPufQ3qZwVO6d6mNK71MaV3qY0rvUxpXepjSu9TGlYJ+ZjcrGUe0X/E8 +3HYX+Mx+P3OB2l/cn4PNEjzuSXmkVbpSRTlZLGXJFk5FpWk5JC0nChOvJG/miyH/ABl/5MvmF2l/ +cS+H3hyNL/eD8dHzn+Yf/Kf+Zv8AtrX3/US+T0391H+qPueoh9Iff3kb/jgx/Mf8m0zzVz2QYq7F +XYq7FXYq7FXyF/zmZ/yneif9ssf9REudh7O/3Uv636A4+bmyz8qv/JHaGf8Aiy5/6jJ8nk/xuXu/ +QHUa1MCczHWLCcKrScKEk/KN+Gqaz/20W/42zV+0Y3x/1Xfdl/SXr31gZzVO0Yv520E+YLSSwbms +EyIHkjKhgUk9Tbl8hmXodXLTZRliATG+fmKas2IZImJ6sFH5J2Q/3ddffF/TOh/0W5/5kPt/W4P8 +lw7ynvlX8v18vXbz25mkMoVX9QpQBWrtxAzV9pdsZNXERkAOHutyNPpI4iSDzei/WBmnpy3fWBjS +u+sDGld9YGNK76wMaV31gY0rvrAxpWGfmA4kt5B/kx/8Tzbdi/4wPj9zgdpf3J+DAkjztCXmldEp +vkbYy5Licm0LScKFhOFU98ib+a7H/nr/AMmXzB7T/wAXl8PvDkaT+8H46PnT8xf/ACYPmf8A7a19 +/wBRL5PTf3Uf6o+56iHIPv3yN/xwY/mP+TaZ5q57IMVdirsVdirsVdir5C/5zMB/x1oh7fosf9RE +udh7O/3Uv636A4+bmyz8qv8AyRuh07S3Ffb/AEyfJz/xuXu/QHUa3kjSczXWLScKFpOFDH/ywfhq +OsH/AJf2/W2a72lG+P8AqO+7L+kvT/rXvnMU7R31r3xpXfWvfGld9a98aV31r3xpXfWvfGld9a98 +aV31r3xpXfWvfGld9a98aV31r3xpWM+bpPUiYeyf8Szadj/4wPj9zg9pf3J+DFUjzsCXmVVkpGTg +id2MuSHJy9oWE4VWk4UJ95CqfNljQbD1a/8AIl8wO1P8Xl8PvDkaP+8H46PnX8xf/Jg+Z/8AtrX3 +/US+T0v91H+qPuephyD798jf8cGP5j/k2meaueyDFXYq7FXYq7FXYq+b/wDnMvyrcXGj6F5ngQtH +YSSWV6QK8VuOLxMfBQ8bLXxYZ0vs7nAlLGeu4+DTmHVif/OOXm+xvdGvfImoTiO5LvdaSXbZlIDS +RINt0ZfUp1ILeGbPtDGYTGUfF12pxcQZ/fafeWUhjuIytDQPT4W+Ry3FljMWC6acDHmhCcta1hOF +Uo/KW39fzBf2/X1dQYU/4LNf7UHfH/Ud92V9Je4/4U/yPwzkuN2tO/wp/kfhjxrTv8Kf5H4Y8a07 +/Cn+R+GPGtO/wp/kfhjxrTv8Kf5H4Y8a07/Cn+R+GPGtO/wp/kfhjxrTv8Kf5H4Y8a07/Cn+R+GP +GtO/wp/kfhjxrTz78wrH6lf/AFelKxI1Pmx/pm27GN5x8fucDtP+5PwYmkedcS8wuuEpbufb+OMD +6mMuSWE5ltK0nChyJJK4jjUu7bKqgkk+wGJIAsqBfJldi1p5F0G982+Yf3BjjMdlZsQsskjbqig/ +tvxoB2FSds0Wu1H5iQxY9+8u20OlINl82eV7HUPNvny1WWs1zqF4bm8cDqC5lmb2rvT3zK1mUYMB +PdGh9wd/AWafoD5TtzBo6L2LEj5ABf8AjXPPHLTjFXYq7FXYq7FXYql/mDQdL8waLeaLqsIuNPv4 +mhuIj3Vu4PZlO6nsd8sxZZY5CUeYQRb4V/NL8oPNv5a656pEs2kiX1NL1uDko+FqpzZf7qVdtvHd +Sc7vQ9o49TGuUusfxzDjTgQmOjf85K/mRp1klrMbLUymy3F5C5loBQAtDJCG+ZFfE4z7KxSN7j3O +OcUSj/8Aoaf8wf8Aq36T/wAibn/soyH8kYu+X2fqR4Ad/wBDT/mD/wBW/Sf+RNz/ANlGP8kYu+X2 +fqXwAoN/zkl5puryK6v9OtRJACIHsXmtXUk9SzvcfgBlObsSEuUiPfv+puxejkjP+hnPMn++bz/u +JS/9U8xv9Dw/n/7H9rd4rv8AoZzzJ/vm8/7iUv8A1Tx/0PD+f/sf2r4rv+hnPMn++bz/ALiUv/VP +H/Q8P5/+x/aviu/6Gc8yf75vP+4lL/1Tx/0PD+f/ALH9q+K7/oZzzJ/vm8/7iUv/AFTx/wBDw/n/ +AOx/aviu/wChnPMn++bz/uJS/wDVPH/Q8P5/+x/aviu/6Gc8yf75vP8AuJS/9U8f9Dw/n/7H9q+K +7/oZzzJ/vm8/7iUv/VPH/Q8P5/8Asf2r4rv+hnPMn++bz/uJS/8AVPH/AEPD+f8A7H9q+K7/AKGc +8yf75vP+4lL/ANU8f9Dw/n/7H9q+K7/oZzzJ/vm8/wC4lL/1Tx/0PD+f/sf2r4qEm/5yR8yi8jvr +awikvEBQyahNLdjgRSg4mBh1/mPyy7D2FCJ3kT7hX62vJLjFK3/Q0/5g/wDVv0n/AJE3P/ZRmT/J +GLvl9n6nH8AO/wChp/zB/wCrfpP/ACJuf+yjH+SMXfL7P1L4Ad/0NP8AmD/1b9J/5E3P/ZRj/JGL +vl9n6l8AO/6Gn/MH/q36T/yJuf8Asox/kjF3y+z9S+AGj/zlP+YJH/HP0ke/o3P/AGUY/wAkYu+X +2fqXwQwPXvM/nfz/AKxF9emm1O7qRa2cS0jiDHf040AVR0qx32+I5lxhi08L2iO9tjCtg+ifyJ/J +ubQF+u36q+tXajmRusEXXiD+vxNPAE8f2r2l+YlUfoH2+f6nKhCn0XBCkEKQxiiRgKv0ZqGxfirs +VdirsVdirsVdiqhfWFlf2slpewpcW0o4yQyKGVh7g4QSNwryzXP+cZ/yy1G4a4i0xIGY1McTyQrX +5RMo/wCFzYY+1tTAUJn40fvYHGEp/wChVPy+/wCWAf8ASXdf1yf8tar+f9kf1L4cXf8AQqn5ff8A +LAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/ +rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n +/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF +3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff +8sA/6S7r+uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r ++uP8tar+f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+ +f9kf1L4cXf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4c +Xf8AQqn5ff8ALAP+ku6/rj/LWq/n/ZH9S+HF3/Qqn5ff8sA/6S7r+uP8tar+f9kf1L4cW1/5xW/L +9WDCwWo33urkj7icT2zqv5/2R/UvhxZl5Z/KLy9oKcLG1t7RduRgT42p4sQN/c5g5tRkym5yMmQA +DNrOytrSL04E4j9o9ST7nKUq+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K +uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku +xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux +V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV//2Q== + + + + + + +uuid:f3c53255-be8a-4b04-817b-695bf2c54c8b + + + +image/svg+xml + + + +filesave.ai + + + + + + +end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labelme/icons/save.png b/labelme/icons/save.png new file mode 100644 index 0000000..daba865 Binary files /dev/null and b/labelme/icons/save.png differ diff --git a/labelme/icons/save.svg b/labelme/icons/save.svg new file mode 100644 index 0000000..5533e48 --- /dev/null +++ b/labelme/icons/save.svg @@ -0,0 +1,679 @@ + + + + + + + + begin='' id='W5M0MpCehiHzreSzNTczkc9d' + + + + +Adobe PDF library 5.00 + + + + + +2004-02-04T02:08:51+02:00 + +2004-03-29T09:20:16Z + +Adobe Illustrator 10.0 + +2004-02-29T14:54:28+01:00 + + + + +JPEG + +256 + +256 + +/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA +AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK +DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER +AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA +AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB +UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE +1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ +qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy +obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp +0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo ++DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 +FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F +XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX +Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY +q7FXzd+b/wDzlWum3k+h+QxFc3EJMdzrkoEkKuNiLZPsyU/nb4fAEb50vZ/YXEBPLsP5v62meXue +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 + + + + + + +uuid:f3c53255-be8a-4b04-817b-695bf2c54c8b + + + +image/svg+xml + + + +filesave.ai + + + + + + end='w' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/labelme/icons/undo-cross.png b/labelme/icons/undo-cross.png new file mode 100644 index 0000000..7d57dcb Binary files /dev/null and b/labelme/icons/undo-cross.png differ diff --git a/labelme/icons/undo.png b/labelme/icons/undo.png new file mode 100644 index 0000000..1813bad Binary files /dev/null and b/labelme/icons/undo.png differ diff --git a/labelme/icons/zoom-in.png b/labelme/icons/zoom-in.png new file mode 100644 index 0000000..1ac4864 Binary files /dev/null and b/labelme/icons/zoom-in.png differ diff --git a/labelme/icons/zoom-out.png b/labelme/icons/zoom-out.png new file mode 100644 index 0000000..d67a87d Binary files /dev/null and b/labelme/icons/zoom-out.png differ diff --git a/labelme/icons/zoom.png b/labelme/icons/zoom.png new file mode 100644 index 0000000..8265f27 Binary files /dev/null and b/labelme/icons/zoom.png differ diff --git a/labelme/label_file.py b/labelme/label_file.py new file mode 100644 index 0000000..209e628 --- /dev/null +++ b/labelme/label_file.py @@ -0,0 +1,198 @@ +import base64 +import contextlib +import io +import json +import os.path as osp + +import PIL.Image + +from labelme import PY2 +from labelme import QT4 +from labelme import __version__ +from labelme import utils +from labelme.logger import logger + +PIL.Image.MAX_IMAGE_PIXELS = None + +@contextlib.contextmanager +def open(name, mode): + assert mode in ["r", "w"] + if PY2: + mode += "b" + encoding = None + else: + encoding = "utf-8" + yield io.open(name, mode, encoding=encoding) + return + + +class LabelFileError(Exception): + pass + + +class LabelFile(object): + suffix = ".json" + + def __init__(self, filename=None): + self.shapes = [] + self.imagePath = None + self.imageData = None + if filename is not None: + self.load(filename) + self.filename = filename + + @staticmethod + def load_image_file(filename): + try: + image_pil = PIL.Image.open(filename) + except IOError: + logger.error("Failed opening image file: {}".format(filename)) + return + + # apply orientation to image according to exif + image_pil = utils.apply_exif_orientation(image_pil) + + with io.BytesIO() as f: + ext = osp.splitext(filename)[1].lower() + if PY2 and QT4: + format = "PNG" + elif ext in [".jpg", ".jpeg"]: + format = "JPEG" + else: + format = "PNG" + image_pil.save(f, format=format) + f.seek(0) + return f.read() + + def load(self, filename): + keys = [ + "version", + "imageData", + "imagePath", + "shapes", # polygonal annotations + "flags", # image level flags + "imageHeight", + "imageWidth", + ] + shape_keys = [ + "label", + "points", + "group_id", + "track_id", + "shape_type", + "flags", + "description", + "mask", + ] + try: + with open(filename, "r") as f: + data = json.load(f) + + if data["imageData"] is not None: + imageData = base64.b64decode(data["imageData"]) + if PY2 and QT4: + imageData = utils.img_data_to_png_data(imageData) + else: + # relative path from label file to relative path from cwd + if osp.isfile(data["imagePath"]): + imagePath = osp.join(osp.dirname(filename), data["imagePath"]) + imageData = self.load_image_file(imagePath) + else: + imagePath = filename.split('.json')[0] + '.jpg' + imageData = self.load_image_file(imagePath) + flags = data.get("flags") or {} + imagePath = data["imagePath"] + self._check_image_height_and_width( + base64.b64encode(imageData).decode("utf-8"), + data.get("imageHeight"), + data.get("imageWidth"), + ) + shapes = [ + dict( + label=s["label"], + points=s["points"], + shape_type=s.get("shape_type", "polygon"), + flags=s.get("flags", {}), + description=s.get("description"), + group_id=s.get("group_id"), + track_id=s.get("track_id"), + mask=utils.img_b64_to_arr(s["mask"]) if s.get("mask") else None, + # other_data={k: v for k, v in s.items() if k not in shape_keys}, + ) + for s in data["shapes"] + ] + except Exception as e: + raise LabelFileError(e) + + otherData = {} + for key, value in data.items(): + if key not in keys: + otherData[key] = value + + # Only replace data after everything is loaded. + self.flags = flags + self.shapes = shapes + self.imagePath = imagePath + self.imageData = imageData + self.filename = filename + self.otherData = otherData + + @staticmethod + def _check_image_height_and_width(imageData, imageHeight, imageWidth): + img_arr = utils.img_b64_to_arr(imageData) + if imageHeight is not None and img_arr.shape[0] != imageHeight: + logger.error( + "imageHeight does not match with imageData or imagePath, " + "so getting imageHeight from actual image." + ) + imageHeight = img_arr.shape[0] + if imageWidth is not None and img_arr.shape[1] != imageWidth: + logger.error( + "imageWidth does not match with imageData or imagePath, " + "so getting imageWidth from actual image." + ) + imageWidth = img_arr.shape[1] + return imageHeight, imageWidth + + def save( + self, + filename, + shapes, + imagePath, + imageHeight, + imageWidth, + imageData=None, + otherData=None, + flags=None, + ): + if imageData is not None: + imageData = base64.b64encode(imageData).decode("utf-8") + imageHeight, imageWidth = self._check_image_height_and_width( + imageData, imageHeight, imageWidth + ) + if otherData is None: + otherData = {} + if flags is None: + flags = {} + data = dict( + version=__version__, + flags=flags, + shapes=shapes, + imagePath=imagePath, + imageData=imageData, + imageHeight=imageHeight, + imageWidth=imageWidth, + ) + for key, value in otherData.items(): + assert key not in data + data[key] = value + try: + with open(filename, "w") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + self.filename = filename + except Exception as e: + raise LabelFileError(e) + + @staticmethod + def is_label_file(filename): + return osp.splitext(filename)[1].lower() == LabelFile.suffix diff --git a/labelme/logger.py b/labelme/logger.py new file mode 100644 index 0000000..25a5ce9 --- /dev/null +++ b/labelme/logger.py @@ -0,0 +1,62 @@ +import datetime +import logging +import os +import sys + +import termcolor + +if os.name == "nt": # Windows + import colorama + + colorama.init() + +from . import __appname__ + +COLORS = { + "WARNING": "yellow", + "INFO": "white", + "DEBUG": "blue", + "CRITICAL": "red", + "ERROR": "red", +} + + +class ColoredFormatter(logging.Formatter): + def __init__(self, fmt, use_color=True): + logging.Formatter.__init__(self, fmt) + self.use_color = use_color + + def format(self, record): + levelname = record.levelname + if self.use_color and levelname in COLORS: + + def colored(text): + return termcolor.colored( + text, + color=COLORS[levelname], + attrs={"bold": True}, + ) + + record.levelname2 = colored("{:<7}".format(record.levelname)) + record.message2 = colored(record.msg) + + asctime2 = datetime.datetime.fromtimestamp(record.created) + record.asctime2 = termcolor.colored(asctime2, color="green") + + record.module2 = termcolor.colored(record.module, color="cyan") + record.funcName2 = termcolor.colored(record.funcName, color="cyan") + record.lineno2 = termcolor.colored(record.lineno, color="cyan") + return logging.Formatter.format(self, record) + + +logger = logging.getLogger(__appname__) +logger.setLevel(logging.INFO) + +stream_handler = logging.StreamHandler(sys.stderr) +handler_format = ColoredFormatter( + "%(asctime)s [%(levelname2)s] %(module2)s:%(funcName2)s:%(lineno2)s" + "- %(message2)s" +) +stream_handler.setFormatter(handler_format) + +logger.addHandler(stream_handler) diff --git a/labelme/shape.py b/labelme/shape.py new file mode 100644 index 0000000..174e1ee --- /dev/null +++ b/labelme/shape.py @@ -0,0 +1,386 @@ +import copy +import math + +import numpy as np +import skimage.measure +from qtpy import QtCore +from qtpy import QtGui + +import labelme.utils +from labelme.logger import logger + +# TODO(unknown): +# - [opt] Store paths instead of creating new ones at each paint. + + +class Shape(object): + # Render handles as squares + P_SQUARE = 0 + + # Render handles as circles + P_ROUND = 1 + + # Flag for the handles we would move if dragging + MOVE_VERTEX = 0 + + # Flag for all other handles on the current shape + NEAR_VERTEX = 1 + + # The following class variables influence the drawing of all shape objects. + line_color = None + fill_color = None + select_line_color = None + select_fill_color = None + vertex_fill_color = None + hvertex_fill_color = None + point_type = P_ROUND + point_size = 8 + scale = 1.0 + + def __init__( + self, + label=None, + line_color=None, + shape_type=None, + flags=None, + group_id=None, + track_id=None, + description=None, + mask=None, + ): + self.label = label + self.group_id = group_id + self.track_id = track_id + self.points = [] + self.point_labels = [] + self.shape_type = shape_type + self._shape_raw = None + self._points_raw = [] + self._shape_type_raw = None + self.fill = False + self.selected = False + self.shape_type = shape_type + self.flags = flags + self.description = description + self.other_data = {} + self.mask = mask + + self._highlightIndex = None + self._highlightMode = self.NEAR_VERTEX + self._highlightSettings = { + self.NEAR_VERTEX: (4, self.P_ROUND), + self.MOVE_VERTEX: (1.5, self.P_SQUARE), + } + + self._closed = False + + if line_color is not None: + # Override the class line_color attribute + # with an object attribute. Currently this + # is used for drawing the pending line a different color. + self.line_color = line_color + + def setShapeRefined(self, shape_type, points, point_labels, mask=None): + self._shape_raw = (self.shape_type, self.points, self.point_labels) + self.shape_type = shape_type + self.points = points + self.point_labels = point_labels + self.mask = mask + + def restoreShapeRaw(self): + if self._shape_raw is None: + return + self.shape_type, self.points, self.point_labels = self._shape_raw + self._shape_raw = None + + @property + def shape_type(self): + return self._shape_type + + @shape_type.setter + def shape_type(self, value): + if value is None: + value = "polygon" + if value not in [ + "polygon", + "rectangle", + "point", + "line", + "circle", + "linestrip", + "points", + "mask", + ]: + raise ValueError("Unexpected shape_type: {}".format(value)) + self._shape_type = value + + def close(self): + self._closed = True + + def addPoint(self, point, label=1): + if self.points and point == self.points[0]: + self.close() + else: + self.points.append(point) + self.point_labels.append(label) + + def canAddPoint(self): + return self.shape_type in ["polygon", "linestrip"] + + def popPoint(self): + if self.points: + if self.point_labels: + self.point_labels.pop() + return self.points.pop() + return None + + def insertPoint(self, i, point, label=1): + self.points.insert(i, point) + self.point_labels.insert(i, label) + + def removePoint(self, i): + if not self.canAddPoint(): + logger.warning( + "Cannot remove point from: shape_type=%r", + self.shape_type, + ) + return + + if self.shape_type == "polygon" and len(self.points) <= 3: + logger.warning( + "Cannot remove point from: shape_type=%r, len(points)=%d", + self.shape_type, + len(self.points), + ) + return + + if self.shape_type == "linestrip" and len(self.points) <= 2: + logger.warning( + "Cannot remove point from: shape_type=%r, len(points)=%d", + self.shape_type, + len(self.points), + ) + return + + self.points.pop(i) + self.point_labels.pop(i) + + def isClosed(self): + return self._closed + + def setOpen(self): + self._closed = False + + def getRectFromLine(self, pt1, pt2): + x1, y1 = pt1.x(), pt1.y() + x2, y2 = pt2.x(), pt2.y() + return QtCore.QRectF(x1, y1, x2 - x1, y2 - y1) + + def paint(self, painter): + if self.mask is None and not self.points: + return + + color = self.select_line_color if self.selected else self.line_color + pen = QtGui.QPen(color) + # Try using integer sizes for smoother drawing(?) + pen.setWidth(max(3, int(round(2.0 / self.scale)))) + painter.setPen(pen) + + if self.mask is not None: + image_to_draw = np.zeros(self.mask.shape + (4,), dtype=np.uint8) + fill_color = ( + self.select_fill_color.getRgb() + if self.selected + else self.fill_color.getRgb() + ) + image_to_draw[self.mask] = fill_color + qimage = QtGui.QImage.fromData(labelme.utils.img_arr_to_data(image_to_draw)) + painter.drawImage( + int(round(self.points[0].x())), + int(round(self.points[0].y())), + qimage, + ) + + line_path = QtGui.QPainterPath() + contours = skimage.measure.find_contours(np.pad(self.mask, pad_width=1)) + for contour in contours: + contour += [self.points[0].y(), self.points[0].x()] + line_path.moveTo(contour[0, 1], contour[0, 0]) + for point in contour[1:]: + line_path.lineTo(point[1], point[0]) + painter.drawPath(line_path) + + if self.points: + line_path = QtGui.QPainterPath() + vrtx_path = QtGui.QPainterPath() + negative_vrtx_path = QtGui.QPainterPath() + + if self.shape_type in ["rectangle", "mask"]: + assert len(self.points) in [1, 2] + if len(self.points) == 2: + rectangle = self.getRectFromLine(*self.points) + line_path.addRect(rectangle) + if self.shape_type == "rectangle": + for i in range(len(self.points)): + self.drawVertex(vrtx_path, i) + elif self.shape_type == "circle": + assert len(self.points) in [1, 2] + if len(self.points) == 2: + rectangle = self.getCircleRectFromLine(self.points) + line_path.addEllipse(rectangle) + for i in range(len(self.points)): + self.drawVertex(vrtx_path, i) + elif self.shape_type == "linestrip": + line_path.moveTo(self.points[0]) + for i, p in enumerate(self.points): + line_path.lineTo(p) + self.drawVertex(vrtx_path, i) + elif self.shape_type == "points": + assert len(self.points) == len(self.point_labels) + for i, point_label in enumerate(self.point_labels): + if point_label == 1: + self.drawVertex(vrtx_path, i) + else: + self.drawVertex(negative_vrtx_path, i) + else: + line_path.moveTo(self.points[0]) + # Uncommenting the following line will draw 2 paths + # for the 1st vertex, and make it non-filled, which + # may be desirable. + # self.drawVertex(vrtx_path, 0) + + for i, p in enumerate(self.points): + line_path.lineTo(p) + self.drawVertex(vrtx_path, i) + if self.isClosed(): + line_path.lineTo(self.points[0]) + + painter.drawPath(line_path) + if vrtx_path.length() > 0: + painter.drawPath(vrtx_path) + painter.fillPath(vrtx_path, self._vertex_fill_color) + if self.fill and self.mask is None: + color = self.select_fill_color if self.selected else self.fill_color + painter.fillPath(line_path, color) + + pen.setColor(QtGui.QColor(255, 0, 0, 255)) + painter.setPen(pen) + painter.drawPath(negative_vrtx_path) + painter.fillPath(negative_vrtx_path, QtGui.QColor(255, 0, 0, 255)) + + def drawVertex(self, path, i): + d = self.point_size / self.scale + shape = self.point_type + point = self.points[i] + if i == self._highlightIndex: + size, shape = self._highlightSettings[self._highlightMode] + d *= size + if self._highlightIndex is not None: + self._vertex_fill_color = self.hvertex_fill_color + else: + self._vertex_fill_color = self.vertex_fill_color + if shape == self.P_SQUARE: + path.addRect(point.x() - d / 2, point.y() - d / 2, d, d) + elif shape == self.P_ROUND: + path.addEllipse(point, d / 2.0, d / 2.0) + else: + assert False, "unsupported vertex shape" + + def nearestVertex(self, point, epsilon): + min_distance = float("inf") + min_i = None + for i, p in enumerate(self.points): + dist = labelme.utils.distance(p - point) + if dist <= epsilon and dist < min_distance: + min_distance = dist + min_i = i + return min_i + + def nearestEdge(self, point, epsilon): + min_distance = float("inf") + post_i = None + for i in range(len(self.points)): + line = [self.points[i - 1], self.points[i]] + dist = labelme.utils.distancetoline(point, line) + if dist <= epsilon and dist < min_distance: + min_distance = dist + post_i = i + return post_i + + def containsPoint(self, point): + if self.mask is not None: + y = np.clip( + int(round(point.y() - self.points[0].y())), + 0, + self.mask.shape[0] - 1, + ) + x = np.clip( + int(round(point.x() - self.points[0].x())), + 0, + self.mask.shape[1] - 1, + ) + return self.mask[y, x] + return self.makePath().contains(point) + + def getCircleRectFromLine(self, line): + """Computes parameters to draw with `QPainterPath::addEllipse`""" + if len(line) != 2: + return None + (c, point) = line + r = line[0] - line[1] + d = math.sqrt(math.pow(r.x(), 2) + math.pow(r.y(), 2)) + rectangle = QtCore.QRectF(c.x() - d, c.y() - d, 2 * d, 2 * d) + return rectangle + + def makePath(self): + if self.shape_type in ["rectangle", "mask"]: + path = QtGui.QPainterPath() + if len(self.points) == 2: + rectangle = self.getRectFromLine(*self.points) + path.addRect(rectangle) + elif self.shape_type == "circle": + path = QtGui.QPainterPath() + if len(self.points) == 2: + rectangle = self.getCircleRectFromLine(self.points) + path.addEllipse(rectangle) + else: + path = QtGui.QPainterPath(self.points[0]) + for p in self.points[1:]: + path.lineTo(p) + return path + + def boundingRect(self): + return self.makePath().boundingRect() + + def moveBy(self, offset): + self.points = [p + offset for p in self.points] + + def moveVertexBy(self, i, offset): + self.points[i] = self.points[i] + offset + + def highlightVertex(self, i, action): + """Highlight a vertex appropriately based on the current action + + Args: + i (int): The vertex index + action (int): The action + (see Shape.NEAR_VERTEX and Shape.MOVE_VERTEX) + """ + self._highlightIndex = i + self._highlightMode = action + + def highlightClear(self): + """Clear the highlighted point""" + self._highlightIndex = None + + def copy(self): + return copy.deepcopy(self) + + def __len__(self): + return len(self.points) + + def __getitem__(self, key): + return self.points[key] + + def __setitem__(self, key, value): + self.points[key] = value diff --git a/labelme/testing.py b/labelme/testing.py new file mode 100644 index 0000000..d865b08 --- /dev/null +++ b/labelme/testing.py @@ -0,0 +1,34 @@ +import json +import os.path as osp + +import imgviz + +import labelme.utils + + +def assert_labelfile_sanity(filename): + assert osp.exists(filename) + + data = json.load(open(filename)) + + assert "imagePath" in data + imageData = data.get("imageData", None) + if imageData is None: + parent_dir = osp.dirname(filename) + img_file = osp.join(parent_dir, data["imagePath"]) + assert osp.exists(img_file) + img = imgviz.io.imread(img_file) + else: + img = labelme.utils.img_b64_to_arr(imageData) + + H, W = img.shape[:2] + assert H == data["imageHeight"] + assert W == data["imageWidth"] + + assert "shapes" in data + for shape in data["shapes"]: + assert "label" in shape + assert "points" in shape + for x, y in shape["points"]: + assert 0 <= x <= W + assert 0 <= y <= H diff --git a/labelme/track_algo/__init__.py b/labelme/track_algo/__init__.py new file mode 100644 index 0000000..befac26 --- /dev/null +++ b/labelme/track_algo/__init__.py @@ -0,0 +1,2 @@ +from .sort import Sort as SORT_main +from .sort import KalmanBoxTracker \ No newline at end of file diff --git a/labelme/track_algo/sort.py b/labelme/track_algo/sort.py new file mode 100644 index 0000000..1afaab9 --- /dev/null +++ b/labelme/track_algo/sort.py @@ -0,0 +1,254 @@ +""" + SORT: A Simple, Online and Realtime Tracker + Copyright (C) 2016-2020 Alex Bewley alex@bewley.ai + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" +from __future__ import print_function + +import os +import numpy as np +from skimage import io + +import glob +import time +import argparse +from filterpy.kalman import KalmanFilter + +np.random.seed(0) + + +def linear_assignment(cost_matrix): + try: + import lap + _, x, y = lap.lapjv(cost_matrix, extend_cost=True) + return np.array([[y[i],i] for i in x if i >= 0]) # + except ImportError: + from scipy.optimize import linear_sum_assignment + x, y = linear_sum_assignment(cost_matrix) + return np.array(list(zip(x, y))) + + +def iou_batch(bb_test, bb_gt): + """ + From SORT: Computes IOU between two bboxes in the form [x1,y1,x2,y2] + """ + bb_gt = np.expand_dims(bb_gt, 0) + bb_test = np.expand_dims(bb_test, 1) + + xx1 = np.maximum(bb_test[..., 0], bb_gt[..., 0]) + yy1 = np.maximum(bb_test[..., 1], bb_gt[..., 1]) + xx2 = np.minimum(bb_test[..., 2], bb_gt[..., 2]) + yy2 = np.minimum(bb_test[..., 3], bb_gt[..., 3]) + w = np.maximum(0., xx2 - xx1) + h = np.maximum(0., yy2 - yy1) + wh = w * h + o = wh / ((bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1]) + + (bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - wh) + return(o) + + +def convert_bbox_to_z(bbox): + """ + Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form + [x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is + the aspect ratio + """ + w = bbox[2] - bbox[0] + h = bbox[3] - bbox[1] + x = bbox[0] + w/2. + y = bbox[1] + h/2. + s = w * h #scale is just area + r = w / float(h) + return np.array([x, y, s, r]).reshape((4, 1)) + + +def convert_x_to_bbox(x,score=None): + """ + Takes a bounding box in the centre form [x,y,s,r] and returns it in the form + [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right + """ + w = np.sqrt(x[2] * x[3]) + h = x[2] / w + if(score==None): + return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.]).reshape((1,4)) + else: + return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.,score]).reshape((1,5)) + + +class KalmanBoxTracker(object): + """ + This class represents the internal state of individual tracked objects observed as bbox. + """ + count = 0 + def __init__(self,bbox,id=None): + """ + Initialises a tracker using initial bounding box. + """ + #define constant velocity model + self.kf = KalmanFilter(dim_x=7, dim_z=4) + self.kf.F = np.array([[1,0,0,0,1,0,0],[0,1,0,0,0,1,0],[0,0,1,0,0,0,1],[0,0,0,1,0,0,0], [0,0,0,0,1,0,0],[0,0,0,0,0,1,0],[0,0,0,0,0,0,1]]) + self.kf.H = np.array([[1,0,0,0,0,0,0],[0,1,0,0,0,0,0],[0,0,1,0,0,0,0],[0,0,0,1,0,0,0]]) + + self.kf.R[2:,2:] *= 10. + self.kf.P[4:,4:] *= 1000. #give high uncertainty to the unobservable initial velocities + self.kf.P *= 10. + self.kf.Q[-1,-1] *= 0.01 + self.kf.Q[4:,4:] *= 0.01 + + self.kf.x[:4] = convert_bbox_to_z(bbox) + self.time_since_update = 0 + if id is None: + self.id = KalmanBoxTracker.count + KalmanBoxTracker.count += 1 + else: + self.id = id + self.history = [] + self.hits = 0 + self.hit_streak = 0 + self.age = 0 + + def update(self,bbox): + """ + Updates the state vector with observed bbox. + """ + self.time_since_update = 0 + self.history = [] + self.hits += 1 + self.hit_streak += 1 + self.kf.update(convert_bbox_to_z(bbox)) + + def predict(self): + """ + Advances the state vector and returns the predicted bounding box estimate. + """ + if((self.kf.x[6]+self.kf.x[2])<=0): + self.kf.x[6] *= 0.0 + self.kf.predict() + self.age += 1 + if(self.time_since_update>0): + self.hit_streak = 0 + self.time_since_update += 1 + self.history.append(convert_x_to_bbox(self.kf.x)) + return self.history[-1] + + def get_state(self): + """ + Returns the current bounding box estimate. + """ + return convert_x_to_bbox(self.kf.x) + + +def associate_detections_to_trackers(detections,trackers,iou_threshold = 0.3): + """ + Assigns detections to tracked object (both represented as bounding boxes) + + Returns 3 lists of matches, unmatched_detections and unmatched_trackers + """ + if(len(trackers)==0): + return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int) + + iou_matrix = iou_batch(detections, trackers) + + if min(iou_matrix.shape) > 0: + a = (iou_matrix > iou_threshold).astype(np.int32) + if a.sum(1).max() == 1 and a.sum(0).max() == 1: + matched_indices = np.stack(np.where(a), axis=1) + else: + matched_indices = linear_assignment(-iou_matrix) + else: + matched_indices = np.empty(shape=(0,2)) + + unmatched_detections = [] + for d, det in enumerate(detections): + if(d not in matched_indices[:,0]): + unmatched_detections.append(d) + unmatched_trackers = [] + for t, trk in enumerate(trackers): + if(t not in matched_indices[:,1]): + unmatched_trackers.append(t) + + #filter out matched with low IOU + matches = [] + for m in matched_indices: + if(iou_matrix[m[0], m[1]]= self.min_hits or self.frame_count <= self.min_hits): + ret.append(np.concatenate((d,[trk.id])).reshape(1,-1)) # +1 as MOT benchmark requires positive + i -= 1 + # remove dead tracklet + if(trk.time_since_update > self.max_age): + self.trackers.pop(i) + if(len(ret)>0): + return np.concatenate(ret) + return np.empty((0,5)) diff --git a/labelme/translate/empty.ts b/labelme/translate/empty.ts new file mode 100644 index 0000000..4f608c1 --- /dev/null +++ b/labelme/translate/empty.ts @@ -0,0 +1,617 @@ + + + + Canvas + + + Image + + + + + Click & drag to move point + + + + + Click & drag to move shape '%s' + + + + + MainWindow + + + Flags + + + + + Polygon Labels + + + + + Select label to start annotating for it. Press 'Esc' to deselect. + + + + + Label List + + + + + Search Filename + + + + + File List + + + + + &Quit + + + + + Quit application + + + + + &Open + + + + + Open image or label file + + + + + &Open Dir + + + + + Open Dir + + + + + &Next Image + + + + + Open next (hold Ctl+Shift to copy labels) + + + + + &Prev Image + + + + + Open prev (hold Ctl+Shift to copy labels) + + + + + &Save + + + + + Save labels to file + + + + + &Save As + + + + + Save labels to a different file + + + + + &Delete File + + + + + Delete current label file + + + + + &Change Output Dir + + + + + Change where annotations are loaded/saved + + + + + Save &Automatically + + + + + Save automatically + + + + + &Close + + + + + Close current file + + + + + Polygon &Line Color + + + + + Choose polygon line color + + + + + Polygon &Fill Color + + + + + Choose polygon fill color + + + + + Keep Previous Annotation + + + + + Toggle "keep pevious annotation" mode + + + + + Create Polygons + + + + + Start drawing polygons + + + + + Create Rectangle + + + + + Start drawing rectangles + + + + + Create Circle + + + + + Start drawing circles + + + + + Create Line + + + + + Start drawing lines + + + + + Create Point + + + + + Start drawing points + + + + + Create LineStrip + + + + + Start drawing linestrip. Ctrl+LeftClick ends creation. + + + + + Edit Polygons + + + + + Move and edit the selected polygons + + + + + Delete Polygons + + + + + Delete the selected polygons + + + + + Duplicate Polygons + + + + + Create a duplicate of the selected polygons + + + + + Undo last point + + + + + Undo last drawn point + + + + + Add Point to Edge + + + + + Add point to the nearest edge + + + + + Undo + + + + + Undo last add and edit of shape + + + + + &Hide +Polygons + + + + + Hide all polygons + + + + + &Show +Polygons + + + + + Show all polygons + + + + + &Toggle +Polygons + + + + + Toggle all polygons + + + + + &Tutorial + + + + + Show tutorial page + + + + + Zoom in or out of the image. Also accessible with {} and {} from the canvas. + + + + + Ctrl+Wheel + + + + + Zoom &In + + + + + Increase zoom level + + + + + &Zoom Out + + + + + Decrease zoom level + + + + + &Original size + + + + + Zoom to original size + + + + + &Fit Window + + + + + Zoom follows window size + + + + + Fit &Width + + + + + Zoom follows window width + + + + + &Edit Label + + + + + Modify the label of the selected polygon + + + + + Shape &Line Color + + + + + Change the line color for this specific shape + + + + + Shape &Fill Color + + + + + Change the fill color for this specific shape + + + + + Fill Drawing Polygon + + + + + Fill polygon while drawing + + + + + &File + + + + + &Edit + + + + + &View + + + + + &Help + + + + + Open &Recent + + + + + %s started. + + + + + Invalid label + + + + + Invalid label '{}' with validation type '{}' + + + + + Error saving label data + + + + + <b>%s</b> + + + + + Error opening file + + + + + No such file: <b>%s</b> + + + + + Loading %s... + + + + + <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file. + + + + + Error reading %s + + + + + <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> + + + + + Loaded %s + + + + + Image & Label files (%s) + + + + + %s - Choose Image or Label file + + + + + %s - Save/Load Annotations in Directory + + + + + %s . Annotations will be saved/loaded in %s + + + + + %s - Choose File + + + + + Label files (*%s) + + + + + Choose File + + + + + You are about to permanently delete this label file, proceed anyway? + + + + + Attention + + + + + Save annotations to "{}" before closing? + + + + + Save annotations? + + + + + Choose line color + + + + + Choose fill color + + + + + You are about to permanently delete {} polygons, proceed anyway? + + + + + %s - Open Directory + + + + diff --git a/labelme/translate/zh_CN.qm b/labelme/translate/zh_CN.qm new file mode 100644 index 0000000..daf21f9 Binary files /dev/null and b/labelme/translate/zh_CN.qm differ diff --git a/labelme/translate/zh_CN.ts b/labelme/translate/zh_CN.ts new file mode 100644 index 0000000..65a53fb --- /dev/null +++ b/labelme/translate/zh_CN.ts @@ -0,0 +1,617 @@ + + + + Canvas + + + Image + 图像 + + + + Click & drag to move point + 点击并拖拽以移动控制点 + + + + Click & drag to move shape '%s' + 点击并拖拽以移动形状'%s' + + + + MainWindow + + + Flags + 标记 + + + + Polygon Labels + 多边形标签 + + + + Select label to start annotating for it. Press 'Esc' to deselect. + 选择标签类型并开始以其标注。按'Esc'取消选择。 + + + + Label List + 标签列表 + + + + Search Filename + 按文件名检索 + + + + File List + 文件列表 + + + + &Quit + 退出(&Q) + + + + Quit application + 退出应用 + + + + &Open + 打开(&O) + + + + Open image or label file + 打开图像或标签文件 + + + + &Open Dir + 打开目录(&O) + + + + Open Dir + 打开目录 + + + + &Next Image + 下一幅(&N) + + + + Open next (hold Ctl+Shift to copy labels) + 打开下一幅 (按Ctl+Shift拷贝标签) + + + + &Prev Image + 上一幅(&P) + + + + Open prev (hold Ctl+Shift to copy labels) + 打开上一幅 (按Ctl+Shift拷贝标签) + + + + &Save + 保存(&S) + + + + Save labels to file + 保存标签到文件 + + + + &Save As + 另存为(&S) + + + + Save labels to a different file + 保存标签到不同的文件 + + + + &Delete File + 删除(&D) + + + + Delete current label file + 删除当前标签文件 + + + + &Change Output Dir + 更改输出路径(&C) + + + + Change where annotations are loaded/saved + 更改载入、保存标注的路径 + + + + Save &Automatically + 自动保存(&A) + + + + Save automatically + 自动保存 + + + + &Close + 关闭(&C) + + + + Close current file + 关闭当前文件 + + + + Polygon &Line Color + 多边形描边颜色(&L) + + + + Choose polygon line color + 选择多边形描边颜色 + + + + Polygon &Fill Color + 多边形填充颜色(&F) + + + + Choose polygon fill color + 选择多边形填充颜色 + + + + Keep Previous Annotation + 保留最后的标注 + + + + Toggle "keep pevious annotation" mode + 开关“保留最后的标注”模式 + + + + Create Polygons + 创建多边形 + + + + Start drawing polygons + 开始绘制多边形 + + + + Create Rectangle + 创建矩形 + + + + Start drawing rectangles + 开始绘制矩形 + + + + Create Circle + 创建圆形 + + + + Start drawing circles + 开始绘制圆形 + + + + Create Line + 创建直线 + + + + Start drawing lines + 开始创建直线 + + + + Create Point + 创建控制点 + + + + Start drawing points + 开始绘制控制点 + + + + Create LineStrip + 创建折线 + + + + Start drawing linestrip. Ctrl+LeftClick ends creation. + 开始绘制折线。Ctrl+单击左键结束绘制。 + + + + Edit Polygons + 编辑多边形 + + + + Move and edit the selected polygons + 移动、编辑选中的多边形 + + + + Delete Polygons + 删除多边形 + + + + Delete the selected polygons + 删除选中的多边形 + + + + Duplicate Polygons + 复制多边形 + + + + Create a duplicate of the selected polygons + 为选中的多边形创建副本 + + + + Undo last point + 撤销最后的控制点 + + + + Undo last drawn point + 撤销最后一次绘制的控制点 + + + + Add Point to Edge + 在边上加入控制点 + + + + Add point to the nearest edge + 在最近的边上加一个控制点 + + + + Undo + 撤销 + + + + Undo last add and edit of shape + 撤销最近一次添加和编辑 + + + + &Hide +Polygons + 隐藏多边形(&H) + + + + Hide all polygons + 隐藏多边形(&H) + + + + &Show +Polygons + 显示多边形(&S) + + + + Show all polygons + 显示所有多边形 + + + + &Toggle +Polygons + 开关多边形(&S) + + + + Toggle all polygons + 开关所有多边形 + + + + &Tutorial + 教程[&T] + + + + Show tutorial page + 显示教程网页 + + + + Zoom in or out of the image. Also accessible with {} and {} from the canvas. + 缩放图像。亦可从画布的{}和{}访问 + + + + Ctrl+Wheel + Ctrl+滚轮 + + + + Zoom &In + 放大(&I) + + + + Increase zoom level + 增加缩放水平 + + + + &Zoom Out + 缩小(&Z) + + + + Decrease zoom level + 减小缩放水平 + + + + &Original size + 原始大小(&O) + + + + Zoom to original size + 缩放至原始大小 + + + + &Fit Window + 适应窗口(&F) + + + + Zoom follows window size + 跟随窗口大小缩放 + + + + Fit &Width + 适应宽度(&W) + + + + Zoom follows window width + 跟随窗口宽度缩放 + + + + &Edit Label + 编辑标签(&E) + + + + Modify the label of the selected polygon + 修改选中多边形的标签 + + + + Shape &Line Color + 形状描边颜色(&L) + + + + Change the line color for this specific shape + 为此多边形修改描边颜色 + + + + Shape &Fill Color + 形状填充颜色(&F) + + + + Change the fill color for this specific shape + 为此多边形修改填充颜色 + + + + Fill Drawing Polygon + 填充所绘多边形 + + + + Fill polygon while drawing + 绘制时填充多边形 + + + + &File + 文件(&F) + + + + &Edit + 编辑(&E) + + + + &View + 视图(&V) + + + + &Help + 帮助(&H) + + + + Open &Recent + 最近打开(&R) + + + + %s started. + %s 启动完了 + + + + Invalid label + 无效的标签 + + + + Invalid label '{}' with validation type '{}' + 无效的标签'{}',验证类型'{}' + + + + Error saving label data + 保存标签发生错误 + + + + <b>%s</b> + <b>%s</b> + + + + Error opening file + 打开文件发生错误 + + + + No such file: <b>%s</b> + 文件不存在: <b>%s</b> + + + + Loading %s... + 正在载入 %s... + + + + <p><b>%s</b></p><p>Make sure <i>%s</i> is a valid label file. + <p><b>%s</b></p><p>请确认<i>%s</i>是一个合法的标签文件。 + + + + Error reading %s + 打开文件发生错误 %s + + + + <p>Make sure <i>{0}</i> is a valid image file.<br/>Supported image formats: {1}</p> + lt;p>请确认<i>{0}</i>是一个合法的图像文件。<br/>支持的格式包括: {1}</p> + + + + Loaded %s + 已加载 %s + + + + Image & Label files (%s) + 图像和标签文件(%s) + + + + %s - Choose Image or Label file + %s - 选择图像或标签文件 + + + + %s - Save/Load Annotations in Directory + %s - 保存和加载批注的路径 + + + + %s . Annotations will be saved/loaded in %s + %s . 批注会被加载和保存在 %s + + + + %s - Choose File + %s - 选择文件 + + + + Label files (*%s) + 标签文件(*%s) + + + + Choose File + 选择文件 + + + + You are about to permanently delete this label file, proceed anyway? + 即将永久性删除此标签文件。还要继续吗? + + + + Attention + 注意 + + + + Save annotations to "{}" before closing? + 关闭前保存批注到"{}"吗? + + + + Save annotations? + 保存批注吗? + + + + Choose line color + 选择描边颜色 + + + + Choose fill color + 选择填充颜色 + + + + You are about to permanently delete {} polygons, proceed anyway? + 即将永久性删除多边形{}。还要继续吗? + + + + %s - Open Directory + %s - 打开目录 + + + diff --git a/labelme/utils/__init__.py b/labelme/utils/__init__.py new file mode 100644 index 0000000..7fa2154 --- /dev/null +++ b/labelme/utils/__init__.py @@ -0,0 +1,29 @@ +# flake8: noqa + +from ._io import lblsave + +from .image import apply_exif_orientation +from .image import img_arr_to_b64 +from .image import img_arr_to_data +from .image import img_b64_to_arr +from .image import img_data_to_arr +from .image import img_data_to_pil +from .image import img_data_to_png_data +from .image import img_pil_to_data +from .image import img_qt_to_arr + +from .shape import labelme_shapes_to_label +from .shape import masks_to_bboxes +from .shape import polygons_to_mask +from .shape import shape_to_mask +from .shape import shapes_to_label + +from .qt import newIcon +from .qt import newButton +from .qt import newAction +from .qt import addActions +from .qt import labelValidator +from .qt import struct +from .qt import distance +from .qt import distancetoline +from .qt import fmtShortcut diff --git a/labelme/utils/_io.py b/labelme/utils/_io.py new file mode 100644 index 0000000..cf869e1 --- /dev/null +++ b/labelme/utils/_io.py @@ -0,0 +1,23 @@ +import os.path as osp + +import numpy as np +import PIL.Image + + +def lblsave(filename, lbl): + import imgviz + + if osp.splitext(filename)[1] != ".png": + filename += ".png" + # Assume label ranses [-1, 254] for int32, + # and [0, 255] for uint8 as VOC. + if lbl.min() >= -1 and lbl.max() < 255: + lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode="P") + colormap = imgviz.label_colormap() + lbl_pil.putpalette(colormap.flatten()) + lbl_pil.save(filename) + else: + raise ValueError( + "[%s] Cannot save the pixel-wise class label as PNG. " + "Please consider using the .npy format." % filename + ) diff --git a/labelme/utils/image.py b/labelme/utils/image.py new file mode 100644 index 0000000..087953e --- /dev/null +++ b/labelme/utils/image.py @@ -0,0 +1,104 @@ +import base64 +import io + +import numpy as np +import PIL.ExifTags +import PIL.Image +import PIL.ImageOps + + +def img_data_to_pil(img_data): + f = io.BytesIO() + f.write(img_data) + img_pil = PIL.Image.open(f) + return img_pil + + +def img_data_to_arr(img_data): + img_pil = img_data_to_pil(img_data) + img_arr = np.array(img_pil) + return img_arr + + +def img_b64_to_arr(img_b64): + img_data = base64.b64decode(img_b64) + img_arr = img_data_to_arr(img_data) + return img_arr + + +def img_pil_to_data(img_pil): + f = io.BytesIO() + img_pil.save(f, format="PNG") + img_data = f.getvalue() + return img_data + + +def img_arr_to_b64(img_arr): + img_data = img_arr_to_data(img_arr) + img_b64 = base64.b64encode(img_data).decode("utf-8") + return img_b64 + + +def img_arr_to_data(img_arr): + img_pil = PIL.Image.fromarray(img_arr) + img_data = img_pil_to_data(img_pil) + return img_data + + +def img_data_to_png_data(img_data): + with io.BytesIO() as f: + f.write(img_data) + img = PIL.Image.open(f) + + with io.BytesIO() as f: + img.save(f, "PNG") + f.seek(0) + return f.read() + + +def img_qt_to_arr(img_qt): + w, h, d = img_qt.size().width(), img_qt.size().height(), img_qt.depth() + bytes_ = img_qt.bits().asstring(w * h * d // 8) + img_arr = np.frombuffer(bytes_, dtype=np.uint8).reshape((h, w, d // 8)) + return img_arr + + +def apply_exif_orientation(image): + try: + exif = image._getexif() + except AttributeError: + exif = None + + if exif is None: + return image + + exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif.items() if k in PIL.ExifTags.TAGS} + + orientation = exif.get("Orientation", None) + + if orientation == 1: + # do nothing + return image + elif orientation == 2: + # left-to-right mirror + return PIL.ImageOps.mirror(image) + elif orientation == 3: + # rotate 180 + return image.transpose(PIL.Image.ROTATE_180) + elif orientation == 4: + # top-to-bottom mirror + return PIL.ImageOps.flip(image) + elif orientation == 5: + # top-to-left mirror + return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270)) + elif orientation == 6: + # rotate 270 + return image.transpose(PIL.Image.ROTATE_270) + elif orientation == 7: + # top-to-right mirror + return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90)) + elif orientation == 8: + # rotate 90 + return image.transpose(PIL.Image.ROTATE_90) + else: + return image diff --git a/labelme/utils/qt.py b/labelme/utils/qt.py new file mode 100644 index 0000000..7fed3ad --- /dev/null +++ b/labelme/utils/qt.py @@ -0,0 +1,98 @@ +import os.path as osp +from math import sqrt + +import numpy as np +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +here = osp.dirname(osp.abspath(__file__)) + + +def newIcon(icon): + icons_dir = osp.join(here, "../icons") + return QtGui.QIcon(osp.join(":/", icons_dir, "%s.png" % icon)) + + +def newButton(text, icon=None, slot=None): + b = QtWidgets.QPushButton(text) + if icon is not None: + b.setIcon(newIcon(icon)) + if slot is not None: + b.clicked.connect(slot) + return b + + +def newAction( + parent, + text, + slot=None, + shortcut=None, + icon=None, + tip=None, + checkable=False, + enabled=True, + checked=False, +): + """Create a new action and assign callbacks, shortcuts, etc.""" + a = QtWidgets.QAction(text, parent) + if icon is not None: + a.setIconText(text.replace(" ", "\n")) + a.setIcon(newIcon(icon)) + if shortcut is not None: + if isinstance(shortcut, (list, tuple)): + a.setShortcuts(shortcut) + else: + a.setShortcut(shortcut) + if tip is not None: + a.setToolTip(tip) + a.setStatusTip(tip) + if slot is not None: + a.triggered.connect(slot) + if checkable: + a.setCheckable(True) + a.setEnabled(enabled) + a.setChecked(checked) + return a + + +def addActions(widget, actions): + for action in actions: + if action is None: + widget.addSeparator() + elif isinstance(action, QtWidgets.QMenu): + widget.addMenu(action) + else: + widget.addAction(action) + + +def labelValidator(): + return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None) + + +class struct(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +def distance(p): + return sqrt(p.x() * p.x() + p.y() * p.y()) + + +def distancetoline(point, line): + p1, p2 = line + p1 = np.array([p1.x(), p1.y()]) + p2 = np.array([p2.x(), p2.y()]) + p3 = np.array([point.x(), point.y()]) + if np.dot((p3 - p1), (p2 - p1)) < 0: + return np.linalg.norm(p3 - p1) + if np.dot((p3 - p2), (p1 - p2)) < 0: + return np.linalg.norm(p3 - p2) + if np.linalg.norm(p2 - p1) == 0: + return np.linalg.norm(p3 - p1) + return np.linalg.norm(np.cross(p2 - p1, p1 - p3)) / np.linalg.norm(p2 - p1) + + +def fmtShortcut(text): + mod, key = text.split("+", 1) + return "%s+%s" % (mod, key) diff --git a/labelme/utils/shape.py b/labelme/utils/shape.py new file mode 100644 index 0000000..020c570 --- /dev/null +++ b/labelme/utils/shape.py @@ -0,0 +1,106 @@ +import math +import uuid + +import numpy as np +import PIL.Image +import PIL.ImageDraw + +from labelme.logger import logger + + +def polygons_to_mask(img_shape, polygons, shape_type=None): + logger.warning( + "The 'polygons_to_mask' function is deprecated, " "use 'shape_to_mask' instead." + ) + return shape_to_mask(img_shape, points=polygons, shape_type=shape_type) + + +def shape_to_mask(img_shape, points, shape_type=None, line_width=10, point_size=5): + mask = np.zeros(img_shape[:2], dtype=np.uint8) + mask = PIL.Image.fromarray(mask) + draw = PIL.ImageDraw.Draw(mask) + xy = [tuple(point) for point in points] + if shape_type == "circle": + assert len(xy) == 2, "Shape of shape_type=circle must have 2 points" + (cx, cy), (px, py) = xy + d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2) + draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1) + elif shape_type == "rectangle": + assert len(xy) == 2, "Shape of shape_type=rectangle must have 2 points" + draw.rectangle(xy, outline=1, fill=1) + elif shape_type == "line": + assert len(xy) == 2, "Shape of shape_type=line must have 2 points" + draw.line(xy=xy, fill=1, width=line_width) + elif shape_type == "linestrip": + draw.line(xy=xy, fill=1, width=line_width) + elif shape_type == "point": + assert len(xy) == 1, "Shape of shape_type=point must have 1 points" + cx, cy = xy[0] + r = point_size + draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1) + else: + assert len(xy) > 2, "Polygon must have points more than 2" + draw.polygon(xy=xy, outline=1, fill=1) + mask = np.array(mask, dtype=bool) + return mask + + +def shapes_to_label(img_shape, shapes, label_name_to_value): + cls = np.zeros(img_shape[:2], dtype=np.int32) + ins = np.zeros_like(cls) + instances = [] + for shape in shapes: + points = shape["points"] + label = shape["label"] + group_id = shape.get("group_id") + if group_id is None: + group_id = uuid.uuid1() + shape_type = shape.get("shape_type", None) + + cls_name = label + instance = (cls_name, group_id) + + if instance not in instances: + instances.append(instance) + ins_id = instances.index(instance) + 1 + cls_id = label_name_to_value[cls_name] + + mask = shape_to_mask(img_shape[:2], points, shape_type) + cls[mask] = cls_id + ins[mask] = ins_id + + return cls, ins + + +def labelme_shapes_to_label(img_shape, shapes): + logger.warn( + "labelme_shapes_to_label is deprecated, so please use " "shapes_to_label." + ) + + label_name_to_value = {"_background_": 0} + for shape in shapes: + label_name = shape["label"] + if label_name in label_name_to_value: + label_value = label_name_to_value[label_name] + else: + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + + lbl, _ = shapes_to_label(img_shape, shapes, label_name_to_value) + return lbl, label_name_to_value + + +def masks_to_bboxes(masks): + if masks.ndim != 3: + raise ValueError("masks.ndim must be 3, but it is {}".format(masks.ndim)) + if masks.dtype != bool: + raise ValueError( + "masks.dtype must be bool type, but it is {}".format(masks.dtype) + ) + bboxes = [] + for mask in masks: + where = np.argwhere(mask) + (y1, x1), (y2, x2) = where.min(0), where.max(0) + 1 + bboxes.append((y1, x1, y2, x2)) + bboxes = np.asarray(bboxes, dtype=np.float32) + return bboxes diff --git a/labelme/widgets/__init__.py b/labelme/widgets/__init__.py new file mode 100644 index 0000000..25d760e --- /dev/null +++ b/labelme/widgets/__init__.py @@ -0,0 +1,34 @@ +# flake8: noqa + +from .brightness_contrast_dialog import BrightnessContrastDialog + +from .canvas import Canvas + +from .color_dialog import ColorDialog + +from .file_dialog_preview import FileDialogPreview + +from .label_dialog import LabelDialog +from .label_dialog import LabelQLineEdit + +from .id_dialog import IDDialog +from .id_dialog import IDQLineEdit + +from .track_dialog import TrackDialog +from .navigation_widget import NavigationWidget +from .interpolation_dialog import InterpolationDialog +from .interpolationrefine_widget import IterpolationRefineWidget +from .interpolationrefineinfo_dialog import InterpolationRefineInfo_Dialog +from .deletetrack_dialog import DeletionDialog + +from .label_list_widget import LabelListWidget +from .label_list_widget import LabelListWidgetItem + +from .id_list_widget import IDListWidget +from .id_list_widget import IDListWidgetItem + +from .tool_bar import ToolBar + +from .unique_label_qlist_widget import UniqueLabelQListWidget + +from .zoom_widget import ZoomWidget diff --git a/labelme/widgets/brightness_contrast_dialog.py b/labelme/widgets/brightness_contrast_dialog.py new file mode 100644 index 0000000..1d35dc6 --- /dev/null +++ b/labelme/widgets/brightness_contrast_dialog.py @@ -0,0 +1,45 @@ +import PIL.Image +import PIL.ImageEnhance +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt + +from .. import utils + + +class BrightnessContrastDialog(QtWidgets.QDialog): + def __init__(self, img, callback, parent=None): + super(BrightnessContrastDialog, self).__init__(parent) + self.setModal(True) + self.setWindowTitle("Brightness/Contrast") + + self.slider_brightness = self._create_slider() + self.slider_contrast = self._create_slider() + + formLayout = QtWidgets.QFormLayout() + formLayout.addRow(self.tr("Brightness"), self.slider_brightness) + formLayout.addRow(self.tr("Contrast"), self.slider_contrast) + self.setLayout(formLayout) + + assert isinstance(img, PIL.Image.Image) + self.img = img + self.callback = callback + + def onNewValue(self, value): + brightness = self.slider_brightness.value() / 50.0 + contrast = self.slider_contrast.value() / 50.0 + + img = self.img + img = PIL.ImageEnhance.Brightness(img).enhance(brightness) + img = PIL.ImageEnhance.Contrast(img).enhance(contrast) + + img_data = utils.img_pil_to_data(img) + qimage = QtGui.QImage.fromData(img_data) + self.callback(qimage) + + def _create_slider(self): + slider = QtWidgets.QSlider(Qt.Horizontal) + slider.setRange(0, 150) + slider.setValue(50) + slider.valueChanged.connect(self.onNewValue) + return slider diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py new file mode 100644 index 0000000..9a476f3 --- /dev/null +++ b/labelme/widgets/canvas.py @@ -0,0 +1,1050 @@ +import imgviz +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +import labelme.ai +import labelme.utils +from labelme import QT5 +from labelme.logger import logger +from labelme.shape import Shape + +# TODO(unknown): +# - [maybe] Find optimal epsilon value. + + +CURSOR_DEFAULT = QtCore.Qt.ArrowCursor +CURSOR_POINT = QtCore.Qt.PointingHandCursor +CURSOR_DRAW = QtCore.Qt.CrossCursor +CURSOR_MOVE = QtCore.Qt.ClosedHandCursor +CURSOR_GRAB = QtCore.Qt.OpenHandCursor + +MOVE_SPEED = 5.0 + + +class Canvas(QtWidgets.QWidget): + zoomRequest = QtCore.Signal(int, QtCore.QPoint) + scrollRequest = QtCore.Signal(int, int) + newShape = QtCore.Signal() + selectionChanged = QtCore.Signal(list) + shapeMoved = QtCore.Signal() + drawingPolygon = QtCore.Signal(bool) + vertexSelected = QtCore.Signal(bool) + + CREATE, EDIT = 0, 1 + + # polygon, rectangle, line, or point + _createMode = "polygon" + + _fill_drawing = False + + def __init__(self, *args, **kwargs): + self.epsilon = kwargs.pop("epsilon", 10.0) + self.double_click = kwargs.pop("double_click", "close") + if self.double_click not in [None, "close"]: + raise ValueError( + "Unexpected value for double_click event: {}".format(self.double_click) + ) + self.num_backups = kwargs.pop("num_backups", 10) + self._crosshair = kwargs.pop( + "crosshair", + { + "polygon": False, + "rectangle": True, + "circle": False, + "line": False, + "point": False, + "linestrip": False, + "ai_polygon": False, + "ai_mask": False, + }, + ) + super(Canvas, self).__init__(*args, **kwargs) + # Initialise local state. + self.mode = self.EDIT + self.shapes = [] + self.shapesBackups = [] + self.current = None + self.selectedShapes = [] # save the selected shapes here + self.selectedShapesCopy = [] + # self.line represents: + # - createMode == 'polygon': edge from last point to current + # - createMode == 'rectangle': diagonal line of the rectangle + # - createMode == 'line': the line + # - createMode == 'point': the point + self.line = Shape() + self.prevPoint = QtCore.QPoint() + self.prevMovePoint = QtCore.QPoint() + self.offsets = QtCore.QPoint(), QtCore.QPoint() + self.scale = 1.0 + self.pixmap = QtGui.QPixmap() + self.visible = {} + self._hideBackround = False + self.hideBackround = False + self.hShape = None + self.prevhShape = None + self.hVertex = None + self.prevhVertex = None + self.hEdge = None + self.prevhEdge = None + self.movingShape = False + self.snapping = True + self.hShapeIsSelected = False + self._painter = QtGui.QPainter() + self._cursor = CURSOR_DEFAULT + # Menus: + # 0: right-click without selection and dragging of shapes + # 1: right-click with selection and dragging of shapes + self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu()) + # Set widget options. + self.setMouseTracking(True) + self.setFocusPolicy(QtCore.Qt.WheelFocus) + + self._ai_model = None + + def fillDrawing(self): + return self._fill_drawing + + def setFillDrawing(self, value): + self._fill_drawing = value + + @property + def createMode(self): + return self._createMode + + @createMode.setter + def createMode(self, value): + if value not in [ + "polygon", + "rectangle", + "circle", + "line", + "point", + "linestrip", + "ai_polygon", + "ai_mask", + ]: + raise ValueError("Unsupported createMode: %s" % value) + self._createMode = value + + def initializeAiModel(self, name): + if name not in [model.name for model in labelme.ai.MODELS]: + raise ValueError("Unsupported ai model: %s" % name) + model = [model for model in labelme.ai.MODELS if model.name == name][0] + + if self._ai_model is not None and self._ai_model.name == model.name: + logger.debug("AI model is already initialized: %r" % model.name) + else: + logger.debug("Initializing AI model: %r" % model.name) + self._ai_model = model() + + if self.pixmap is None: + logger.warning("Pixmap is not set yet") + return + + self._ai_model.set_image( + image=labelme.utils.img_qt_to_arr(self.pixmap.toImage()) + ) + + def storeShapes(self): + shapesBackup = [] + for shape in self.shapes: + shapesBackup.append(shape.copy()) + if len(self.shapesBackups) > self.num_backups: + self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :] + self.shapesBackups.append(shapesBackup) + + @property + def isShapeRestorable(self): + # We save the state AFTER each edit (not before) so for an + # edit to be undoable, we expect the CURRENT and the PREVIOUS state + # to be in the undo stack. + if len(self.shapesBackups) < 2: + return False + return True + + def restoreShape(self): + # This does _part_ of the job of restoring shapes. + # The complete process is also done in app.py::undoShapeEdit + # and app.py::loadShapes and our own Canvas::loadShapes function. + if not self.isShapeRestorable: + return + self.shapesBackups.pop() # latest + + # The application will eventually call Canvas.loadShapes which will + # push this right back onto the stack. + shapesBackup = self.shapesBackups.pop() + self.shapes = shapesBackup + self.selectedShapes = [] + for shape in self.shapes: + shape.selected = False + self.update() + + def enterEvent(self, ev): + self.overrideCursor(self._cursor) + + def leaveEvent(self, ev): + self.unHighlight() + self.restoreCursor() + + def focusOutEvent(self, ev): + self.restoreCursor() + + def isVisible(self, shape): + return self.visible.get(shape, True) + + def drawing(self): + return self.mode == self.CREATE + + def editing(self): + return self.mode == self.EDIT + + def setEditing(self, value=True): + self.mode = self.EDIT if value else self.CREATE + if self.mode == self.EDIT: + # CREATE -> EDIT + self.repaint() # clear crosshair + else: + # EDIT -> CREATE + self.unHighlight() + self.deSelectShape() + + def unHighlight(self): + if self.hShape: + self.hShape.highlightClear() + self.update() + self.prevhShape = self.hShape + self.prevhVertex = self.hVertex + self.prevhEdge = self.hEdge + self.hShape = self.hVertex = self.hEdge = None + + def selectedVertex(self): + return self.hVertex is not None + + def selectedEdge(self): + return self.hEdge is not None + + def mouseMoveEvent(self, ev): + """Update line with last point and current coordinates.""" + try: + if QT5: + pos = self.transformPos(ev.localPos()) + else: + pos = self.transformPos(ev.posF()) + except AttributeError: + return + + self.prevMovePoint = pos + self.restoreCursor() + + is_shift_pressed = ev.modifiers() & QtCore.Qt.ShiftModifier + + # Polygon drawing. + if self.drawing(): + if self.createMode in ["ai_polygon", "ai_mask"]: + self.line.shape_type = "points" + else: + self.line.shape_type = self.createMode + + self.overrideCursor(CURSOR_DRAW) + if not self.current: + self.repaint() # draw crosshair + return + + if self.outOfPixmap(pos): + # Don't allow the user to draw outside the pixmap. + # Project the point to the pixmap's edges. + pos = self.intersectionPoint(self.current[-1], pos) + elif ( + self.snapping + and len(self.current) > 1 + and self.createMode == "polygon" + and self.closeEnough(pos, self.current[0]) + ): + # Attract line to starting point and + # colorise to alert the user. + pos = self.current[0] + self.overrideCursor(CURSOR_POINT) + self.current.highlightVertex(0, Shape.NEAR_VERTEX) + if self.createMode in ["polygon", "linestrip"]: + self.line.points = [self.current[-1], pos] + self.line.point_labels = [1, 1] + elif self.createMode in ["ai_polygon", "ai_mask"]: + self.line.points = [self.current.points[-1], pos] + self.line.point_labels = [ + self.current.point_labels[-1], + 0 if is_shift_pressed else 1, + ] + elif self.createMode == "rectangle": + self.line.points = [self.current[0], pos] + self.line.point_labels = [1, 1] + self.line.close() + elif self.createMode == "circle": + self.line.points = [self.current[0], pos] + self.line.point_labels = [1, 1] + self.line.shape_type = "circle" + elif self.createMode == "line": + self.line.points = [self.current[0], pos] + self.line.point_labels = [1, 1] + self.line.close() + elif self.createMode == "point": + self.line.points = [self.current[0]] + self.line.point_labels = [1] + self.line.close() + assert len(self.line.points) == len(self.line.point_labels) + self.repaint() + self.current.highlightClear() + return + + # Polygon copy moving. + if QtCore.Qt.RightButton & ev.buttons(): + if self.selectedShapesCopy and self.prevPoint: + self.overrideCursor(CURSOR_MOVE) + self.boundedMoveShapes(self.selectedShapesCopy, pos) + self.repaint() + elif self.selectedShapes: + self.selectedShapesCopy = [s.copy() for s in self.selectedShapes] + self.repaint() + return + + # Polygon/Vertex moving. + if QtCore.Qt.LeftButton & ev.buttons(): + if self.selectedVertex(): + self.boundedMoveVertex(pos) + self.repaint() + self.movingShape = True + elif self.selectedShapes and self.prevPoint: + self.overrideCursor(CURSOR_MOVE) + self.boundedMoveShapes(self.selectedShapes, pos) + self.repaint() + self.movingShape = True + return + + # Just hovering over the canvas, 2 possibilities: + # - Highlight shapes + # - Highlight vertex + # Update shape/vertex fill and tooltip value accordingly. + self.setToolTip(self.tr("Image")) + for shape in reversed([s for s in self.shapes if self.isVisible(s)]): + # Look for a nearby vertex to highlight. If that fails, + # check if we happen to be inside a shape. + index = shape.nearestVertex(pos, self.epsilon / self.scale) + index_edge = shape.nearestEdge(pos, self.epsilon / self.scale) + if index is not None: + if self.selectedVertex(): + self.hShape.highlightClear() + self.prevhVertex = self.hVertex = index + self.prevhShape = self.hShape = shape + self.prevhEdge = self.hEdge + self.hEdge = None + shape.highlightVertex(index, shape.MOVE_VERTEX) + self.overrideCursor(CURSOR_POINT) + self.setToolTip(self.tr("Click & drag to move point")) + self.setStatusTip(self.toolTip()) + self.update() + break + elif index_edge is not None and shape.canAddPoint(): + if self.selectedVertex(): + self.hShape.highlightClear() + self.prevhVertex = self.hVertex + self.hVertex = None + self.prevhShape = self.hShape = shape + self.prevhEdge = self.hEdge = index_edge + self.overrideCursor(CURSOR_POINT) + self.setToolTip(self.tr("Click to create point")) + self.setStatusTip(self.toolTip()) + self.update() + break + elif shape.containsPoint(pos): + if self.selectedVertex(): + self.hShape.highlightClear() + self.prevhVertex = self.hVertex + self.hVertex = None + self.prevhShape = self.hShape = shape + self.prevhEdge = self.hEdge + self.hEdge = None + self.setToolTip( + self.tr("Click & drag to move shape '%s'") % shape.label + ) + self.setStatusTip(self.toolTip()) + self.overrideCursor(CURSOR_GRAB) + self.update() + break + else: # Nothing found, clear highlights, reset state. + self.unHighlight() + self.vertexSelected.emit(self.hVertex is not None) + + def addPointToEdge(self): + shape = self.prevhShape + index = self.prevhEdge + point = self.prevMovePoint + if shape is None or index is None or point is None: + return + shape.insertPoint(index, point) + shape.highlightVertex(index, shape.MOVE_VERTEX) + self.hShape = shape + self.hVertex = index + self.hEdge = None + self.movingShape = True + + def removeSelectedPoint(self): + shape = self.prevhShape + index = self.prevhVertex + if shape is None or index is None: + return + shape.removePoint(index) + shape.highlightClear() + self.hShape = shape + self.prevhVertex = None + self.movingShape = True # Save changes + + def mousePressEvent(self, ev): + if QT5: + pos = self.transformPos(ev.localPos()) + else: + pos = self.transformPos(ev.posF()) + + is_shift_pressed = ev.modifiers() & QtCore.Qt.ShiftModifier + + if ev.button() == QtCore.Qt.LeftButton: + if self.drawing(): + if self.current: + # Add point to existing shape. + if self.createMode == "polygon": + self.current.addPoint(self.line[1]) + self.line[0] = self.current[-1] + if self.current.isClosed(): + self.finalise() + elif self.createMode in ["rectangle", "circle", "line"]: + assert len(self.current.points) == 1 + self.current.points = self.line.points + self.finalise() + elif self.createMode == "linestrip": + self.current.addPoint(self.line[1]) + self.line[0] = self.current[-1] + if int(ev.modifiers()) == QtCore.Qt.ControlModifier: + self.finalise() + elif self.createMode in ["ai_polygon", "ai_mask"]: + self.current.addPoint( + self.line.points[1], + label=self.line.point_labels[1], + ) + self.line.points[0] = self.current.points[-1] + self.line.point_labels[0] = self.current.point_labels[-1] + if ev.modifiers() & QtCore.Qt.ControlModifier: + self.finalise() + elif not self.outOfPixmap(pos): + # Create new shape. + self.current = Shape( + shape_type="points" + if self.createMode in ["ai_polygon", "ai_mask"] + else self.createMode + ) + self.current.addPoint(pos, label=0 if is_shift_pressed else 1) + if self.createMode == "point": + self.finalise() + elif ( + self.createMode in ["ai_polygon", "ai_mask"] + and ev.modifiers() & QtCore.Qt.ControlModifier + ): + self.finalise() + else: + if self.createMode == "circle": + self.current.shape_type = "circle" + self.line.points = [pos, pos] + if ( + self.createMode in ["ai_polygon", "ai_mask"] + and is_shift_pressed + ): + self.line.point_labels = [0, 0] + else: + self.line.point_labels = [1, 1] + self.setHiding() + self.drawingPolygon.emit(True) + self.update() + elif self.editing(): + if self.selectedEdge(): + self.addPointToEdge() + elif ( + self.selectedVertex() + and int(ev.modifiers()) == QtCore.Qt.ShiftModifier + ): + # Delete point if: left-click + SHIFT on a point + self.removeSelectedPoint() + + group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier + self.selectShapePoint(pos, multiple_selection_mode=group_mode) + self.prevPoint = pos + self.repaint() + elif ev.button() == QtCore.Qt.RightButton and self.editing(): + group_mode = int(ev.modifiers()) == QtCore.Qt.ControlModifier + if not self.selectedShapes or ( + self.hShape is not None and self.hShape not in self.selectedShapes + ): + self.selectShapePoint(pos, multiple_selection_mode=group_mode) + self.repaint() + self.prevPoint = pos + + def mouseReleaseEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + menu = self.menus[len(self.selectedShapesCopy) > 0] + self.restoreCursor() + if not menu.exec_(self.mapToGlobal(ev.pos())) and self.selectedShapesCopy: + # Cancel the move by deleting the shadow copy. + self.selectedShapesCopy = [] + self.repaint() + elif ev.button() == QtCore.Qt.LeftButton: + if self.editing(): + if ( + self.hShape is not None + and self.hShapeIsSelected + and not self.movingShape + ): + self.selectionChanged.emit( + [x for x in self.selectedShapes if x != self.hShape] + ) + + if self.movingShape and self.hShape: + index = self.shapes.index(self.hShape) + if self.shapesBackups[-1][index].points != self.shapes[index].points: + self.storeShapes() + self.shapeMoved.emit() + + self.movingShape = False + + def endMove(self, copy): + assert self.selectedShapes and self.selectedShapesCopy + assert len(self.selectedShapesCopy) == len(self.selectedShapes) + if copy: + for i, shape in enumerate(self.selectedShapesCopy): + self.shapes.append(shape) + self.selectedShapes[i].selected = False + self.selectedShapes[i] = shape + else: + for i, shape in enumerate(self.selectedShapesCopy): + self.selectedShapes[i].points = shape.points + self.selectedShapesCopy = [] + self.repaint() + self.storeShapes() + return True + + def hideBackroundShapes(self, value): + self.hideBackround = value + if self.selectedShapes: + # Only hide other shapes if there is a current selection. + # Otherwise the user will not be able to select a shape. + self.setHiding(True) + self.update() + + def setHiding(self, enable=True): + self._hideBackround = self.hideBackround if enable else False + + def canCloseShape(self): + return self.drawing() and self.current and len(self.current) > 2 + + def mouseDoubleClickEvent(self, ev): + if self.double_click != "close": + return + + if ( + self.createMode == "polygon" and self.canCloseShape() + ) or self.createMode in ["ai_polygon", "ai_mask"]: + self.finalise() + + def selectShapes(self, shapes): + self.setHiding() + self.selectionChanged.emit(shapes) + self.update() + + def selectShapePoint(self, point, multiple_selection_mode): + """Select the first shape created which contains this point.""" + if self.selectedVertex(): # A vertex is marked for selection. + index, shape = self.hVertex, self.hShape + shape.highlightVertex(index, shape.MOVE_VERTEX) + else: + for shape in reversed(self.shapes): + if self.isVisible(shape) and shape.containsPoint(point): + self.setHiding() + if shape not in self.selectedShapes: + if multiple_selection_mode: + self.selectionChanged.emit(self.selectedShapes + [shape]) + else: + self.selectionChanged.emit([shape]) + self.hShapeIsSelected = False + else: + self.hShapeIsSelected = True + self.calculateOffsets(point) + return + self.deSelectShape() + + def calculateOffsets(self, point): + left = self.pixmap.width() - 1 + right = 0 + top = self.pixmap.height() - 1 + bottom = 0 + for s in self.selectedShapes: + rect = s.boundingRect() + if rect.left() < left: + left = rect.left() + if rect.right() > right: + right = rect.right() + if rect.top() < top: + top = rect.top() + if rect.bottom() > bottom: + bottom = rect.bottom() + + x1 = left - point.x() + y1 = top - point.y() + x2 = right - point.x() + y2 = bottom - point.y() + self.offsets = QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2) + + def boundedMoveVertex(self, pos): + index, shape = self.hVertex, self.hShape + point = shape[index] + if self.outOfPixmap(pos): + pos = self.intersectionPoint(point, pos) + shape.moveVertexBy(index, pos - point) + + def boundedMoveShapes(self, shapes, pos): + if self.outOfPixmap(pos): + return False # No need to move + o1 = pos + self.offsets[0] + if self.outOfPixmap(o1): + pos -= QtCore.QPointF(min(0, o1.x()), min(0, o1.y())) + o2 = pos + self.offsets[1] + if self.outOfPixmap(o2): + pos += QtCore.QPointF( + min(0, self.pixmap.width() - o2.x()), + min(0, self.pixmap.height() - o2.y()), + ) + # XXX: The next line tracks the new position of the cursor + # relative to the shape, but also results in making it + # a bit "shaky" when nearing the border and allows it to + # go outside of the shape's area for some reason. + # self.calculateOffsets(self.selectedShapes, pos) + dp = pos - self.prevPoint + if dp: + for shape in shapes: + shape.moveBy(dp) + self.prevPoint = pos + return True + return False + + def deSelectShape(self): + if self.selectedShapes: + self.setHiding(False) + self.selectionChanged.emit([]) + self.hShapeIsSelected = False + self.update() + + def deleteSelected(self): + deleted_shapes = [] + if self.selectedShapes: + for shape in self.selectedShapes: + self.shapes.remove(shape) + deleted_shapes.append(shape) + self.storeShapes() + self.selectedShapes = [] + self.update() + return deleted_shapes + + def deleteShape(self, shape): + if shape in self.selectedShapes: + self.selectedShapes.remove(shape) + if shape in self.shapes: + self.shapes.remove(shape) + self.storeShapes() + self.update() + + def duplicateSelectedShapes(self): + if self.selectedShapes: + self.selectedShapesCopy = [s.copy() for s in self.selectedShapes] + self.boundedShiftShapes(self.selectedShapesCopy) + self.endMove(copy=True) + return self.selectedShapes + + def boundedShiftShapes(self, shapes): + # Try to move in one direction, and if it fails in another. + # Give up if both fail. + point = shapes[0][0] + offset = QtCore.QPointF(2.0, 2.0) + self.offsets = QtCore.QPoint(), QtCore.QPoint() + self.prevPoint = point + if not self.boundedMoveShapes(shapes, point - offset): + self.boundedMoveShapes(shapes, point + offset) + + def paintEvent(self, event): + if not self.pixmap: + return super(Canvas, self).paintEvent(event) + + p = self._painter + p.begin(self) + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) + + p.scale(self.scale, self.scale) + p.translate(self.offsetToCenter()) + + p.drawPixmap(0, 0, self.pixmap) + + # draw crosshair + if ( + self._crosshair[self._createMode] + and self.drawing() + and self.prevMovePoint + and not self.outOfPixmap(self.prevMovePoint) + ): + p.setPen(QtGui.QColor(0, 0, 0)) + p.drawLine( + 0, + int(self.prevMovePoint.y()), + self.width() - 1, + int(self.prevMovePoint.y()), + ) + p.drawLine( + int(self.prevMovePoint.x()), + 0, + int(self.prevMovePoint.x()), + self.height() - 1, + ) + + Shape.scale = self.scale + for shape in self.shapes: + if (shape.selected or not self._hideBackround) and self.isVisible(shape): + shape.fill = shape.selected or shape == self.hShape + shape.paint(p) + if self.current: + self.current.paint(p) + assert len(self.line.points) == len(self.line.point_labels) + self.line.paint(p) + if self.selectedShapesCopy: + for s in self.selectedShapesCopy: + s.paint(p) + + if ( + self.fillDrawing() + and self.createMode == "polygon" + and self.current is not None + and len(self.current.points) >= 2 + ): + drawing_shape = self.current.copy() + if drawing_shape.fill_color.getRgb()[3] == 0: + logger.warning( + "fill_drawing=true, but fill_color is transparent," + " so forcing to be opaque." + ) + drawing_shape.fill_color.setAlpha(64) + drawing_shape.addPoint(self.line[1]) + drawing_shape.fill = True + drawing_shape.paint(p) + elif self.createMode == "ai_polygon" and self.current is not None: + drawing_shape = self.current.copy() + drawing_shape.addPoint( + point=self.line.points[1], + label=self.line.point_labels[1], + ) + points = self._ai_model.predict_polygon_from_points( + points=[[point.x(), point.y()] for point in drawing_shape.points], + point_labels=drawing_shape.point_labels, + ) + if len(points) > 2: + drawing_shape.setShapeRefined( + shape_type="polygon", + points=[QtCore.QPointF(point[0], point[1]) for point in points], + point_labels=[1] * len(points), + ) + drawing_shape.fill = self.fillDrawing() + drawing_shape.selected = True + drawing_shape.paint(p) + elif self.createMode == "ai_mask" and self.current is not None: + drawing_shape = self.current.copy() + drawing_shape.addPoint( + point=self.line.points[1], + label=self.line.point_labels[1], + ) + mask = self._ai_model.predict_mask_from_points( + points=[[point.x(), point.y()] for point in drawing_shape.points], + point_labels=drawing_shape.point_labels, + ) + y1, x1, y2, x2 = imgviz.instances.masks_to_bboxes([mask])[0].astype(int) + drawing_shape.setShapeRefined( + shape_type="mask", + points=[QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)], + point_labels=[1, 1], + mask=mask[y1 : y2 + 1, x1 : x2 + 1], + ) + drawing_shape.selected = True + drawing_shape.paint(p) + + p.end() + + def transformPos(self, point): + """Convert from widget-logical coordinates to painter-logical ones.""" + return point / self.scale - self.offsetToCenter() + + def offsetToCenter(self): + s = self.scale + area = super(Canvas, self).size() + w, h = self.pixmap.width() * s, self.pixmap.height() * s + aw, ah = area.width(), area.height() + x = (aw - w) / (2 * s) if aw > w else 0 + y = (ah - h) / (2 * s) if ah > h else 0 + return QtCore.QPointF(x, y) + + def outOfPixmap(self, p): + w, h = self.pixmap.width(), self.pixmap.height() + return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1) + + def finalise(self): + assert self.current + if self.createMode == "ai_polygon": + # convert points to polygon by an AI model + assert self.current.shape_type == "points" + points = self._ai_model.predict_polygon_from_points( + points=[[point.x(), point.y()] for point in self.current.points], + point_labels=self.current.point_labels, + ) + self.current.setShapeRefined( + points=[QtCore.QPointF(point[0], point[1]) for point in points], + point_labels=[1] * len(points), + shape_type="polygon", + ) + elif self.createMode == "ai_mask": + # convert points to mask by an AI model + assert self.current.shape_type == "points" + mask = self._ai_model.predict_mask_from_points( + points=[[point.x(), point.y()] for point in self.current.points], + point_labels=self.current.point_labels, + ) + y1, x1, y2, x2 = imgviz.instances.masks_to_bboxes([mask])[0].astype(int) + self.current.setShapeRefined( + shape_type="mask", + points=[QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)], + point_labels=[1, 1], + mask=mask[y1 : y2 + 1, x1 : x2 + 1], + ) + self.current.close() + + self.shapes.append(self.current) + self.storeShapes() + self.current = None + self.setHiding(False) + self.newShape.emit() + self.update() + + def closeEnough(self, p1, p2): + # d = distance(p1 - p2) + # m = (p1-p2).manhattanLength() + # print "d %.2f, m %d, %.2f" % (d, m, d - m) + # divide by scale to allow more precision when zoomed in + return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale) + + def intersectionPoint(self, p1, p2): + # Cycle through each image edge in clockwise fashion, + # and find the one intersecting the current line segment. + # http://paulbourke.net/geometry/lineline2d/ + size = self.pixmap.size() + points = [ + (0, 0), + (size.width() - 1, 0), + (size.width() - 1, size.height() - 1), + (0, size.height() - 1), + ] + # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap + x1 = min(max(p1.x(), 0), size.width() - 1) + y1 = min(max(p1.y(), 0), size.height() - 1) + x2, y2 = p2.x(), p2.y() + d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) + x3, y3 = points[i] + x4, y4 = points[(i + 1) % 4] + if (x, y) == (x1, y1): + # Handle cases where previous point is on one of the edges. + if x3 == x4: + return QtCore.QPointF(x3, min(max(0, y2), max(y3, y4))) + else: # y3 == y4 + return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3) + return QtCore.QPointF(x, y) + + def intersectingEdges(self, point1, point2, points): + """Find intersecting edges. + + For each edge formed by `points', yield the intersection + with the line segment `(x1,y1) - (x2,y2)`, if it exists. + Also return the distance of `(x2,y2)' to the middle of the + edge along with its index, so that the one closest can be chosen. + """ + (x1, y1) = point1 + (x2, y2) = point2 + for i in range(4): + x3, y3 = points[i] + x4, y4 = points[(i + 1) % 4] + denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) + nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) + nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) + if denom == 0: + # This covers two cases: + # nua == nub == 0: Coincident + # otherwise: Parallel + continue + ua, ub = nua / denom, nub / denom + if 0 <= ua <= 1 and 0 <= ub <= 1: + x = x1 + ua * (x2 - x1) + y = y1 + ua * (y2 - y1) + m = QtCore.QPointF((x3 + x4) / 2, (y3 + y4) / 2) + d = labelme.utils.distance(m - QtCore.QPointF(x2, y2)) + yield d, i, (x, y) + + # These two, along with a call to adjustSize are required for the + # scroll area. + def sizeHint(self): + return self.minimumSizeHint() + + def minimumSizeHint(self): + if self.pixmap: + return self.scale * self.pixmap.size() + return super(Canvas, self).minimumSizeHint() + + def wheelEvent(self, ev): + if QT5: + mods = ev.modifiers() + delta = ev.angleDelta() + if QtCore.Qt.ControlModifier == int(mods): + # with Ctrl/Command key + # zoom + self.zoomRequest.emit(delta.y(), ev.pos()) + else: + # scroll + self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal) + self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical) + else: + if ev.orientation() == QtCore.Qt.Vertical: + mods = ev.modifiers() + if QtCore.Qt.ControlModifier == int(mods): + # with Ctrl/Command key + self.zoomRequest.emit(ev.delta(), ev.pos()) + else: + self.scrollRequest.emit( + ev.delta(), + QtCore.Qt.Horizontal + if (QtCore.Qt.ShiftModifier == int(mods)) + else QtCore.Qt.Vertical, + ) + else: + self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal) + ev.accept() + + def moveByKeyboard(self, offset): + if self.selectedShapes: + self.boundedMoveShapes(self.selectedShapes, self.prevPoint + offset) + self.repaint() + self.movingShape = True + + def keyPressEvent(self, ev): + modifiers = ev.modifiers() + key = ev.key() + if self.drawing(): + if key == QtCore.Qt.Key_Escape and self.current: + self.current = None + self.drawingPolygon.emit(False) + self.update() + elif key == QtCore.Qt.Key_Return and self.canCloseShape(): + self.finalise() + elif modifiers == QtCore.Qt.AltModifier: + self.snapping = False + elif self.editing(): + if key == QtCore.Qt.Key_Up: + self.moveByKeyboard(QtCore.QPointF(0.0, -MOVE_SPEED)) + elif key == QtCore.Qt.Key_Down: + self.moveByKeyboard(QtCore.QPointF(0.0, MOVE_SPEED)) + elif key == QtCore.Qt.Key_Left: + self.moveByKeyboard(QtCore.QPointF(-MOVE_SPEED, 0.0)) + elif key == QtCore.Qt.Key_Right: + self.moveByKeyboard(QtCore.QPointF(MOVE_SPEED, 0.0)) + + def keyReleaseEvent(self, ev): + modifiers = ev.modifiers() + if self.drawing(): + if int(modifiers) == 0: + self.snapping = True + elif self.editing(): + if self.movingShape and self.selectedShapes: + index = self.shapes.index(self.selectedShapes[0]) + if self.shapesBackups[-1][index].points != self.shapes[index].points: + self.storeShapes() + self.shapeMoved.emit() + + self.movingShape = False + + def setLastLabel(self, text, flags): + assert text + self.shapes[-1].label = text + self.shapes[-1].flags = flags + self.shapesBackups.pop() + self.storeShapes() + return self.shapes[-1] + + def undoLastLine(self): + assert self.shapes + self.current = self.shapes.pop() + self.current.setOpen() + self.current.restoreShapeRaw() + if self.createMode in ["polygon", "linestrip"]: + self.line.points = [self.current[-1], self.current[0]] + elif self.createMode in ["rectangle", "line", "circle"]: + self.current.points = self.current.points[0:1] + elif self.createMode == "point": + self.current = None + self.drawingPolygon.emit(True) + + def undoLastPoint(self): + if not self.current or self.current.isClosed(): + return + self.current.popPoint() + if len(self.current) > 0: + self.line[0] = self.current[-1] + else: + self.current = None + self.drawingPolygon.emit(False) + self.update() + + def loadPixmap(self, pixmap, clear_shapes=True): + self.pixmap = pixmap + if self._ai_model: + self._ai_model.set_image( + image=labelme.utils.img_qt_to_arr(self.pixmap.toImage()) + ) + if clear_shapes: + self.shapes = [] + self.update() + + def loadShapes(self, shapes, replace=True): + if replace: + self.shapes = list(shapes) + else: + self.shapes.extend(shapes) + self.storeShapes() + self.current = None + self.hShape = None + self.hVertex = None + self.hEdge = None + self.update() + + def setShapeVisible(self, shape, value): + self.visible[shape] = value + self.update() + + def overrideCursor(self, cursor): + self.restoreCursor() + self._cursor = cursor + QtWidgets.QApplication.setOverrideCursor(cursor) + + def restoreCursor(self): + QtWidgets.QApplication.restoreOverrideCursor() + + def resetState(self): + self.restoreCursor() + self.pixmap = None + self.shapesBackups = [] + self.update() diff --git a/labelme/widgets/color_dialog.py b/labelme/widgets/color_dialog.py new file mode 100644 index 0000000..f1590a3 --- /dev/null +++ b/labelme/widgets/color_dialog.py @@ -0,0 +1,31 @@ +from qtpy import QtWidgets + + +class ColorDialog(QtWidgets.QColorDialog): + def __init__(self, parent=None): + super(ColorDialog, self).__init__(parent) + self.setOption(QtWidgets.QColorDialog.ShowAlphaChannel) + # The Mac native dialog does not support our restore button. + self.setOption(QtWidgets.QColorDialog.DontUseNativeDialog) + # Add a restore defaults button. + # The default is set at invocation time, so that it + # works across dialogs for different elements. + self.default = None + self.bb = self.layout().itemAt(1).widget() + self.bb.addButton(QtWidgets.QDialogButtonBox.RestoreDefaults) + self.bb.clicked.connect(self.checkRestore) + + def getColor(self, value=None, title=None, default=None): + self.default = default + if title: + self.setWindowTitle(title) + if value: + self.setCurrentColor(value) + return self.currentColor() if self.exec_() else None + + def checkRestore(self, button): + if ( + self.bb.buttonRole(button) & QtWidgets.QDialogButtonBox.ResetRole + and self.default + ): + self.setCurrentColor(self.default) diff --git a/labelme/widgets/deletetrack_dialog.py b/labelme/widgets/deletetrack_dialog.py new file mode 100644 index 0000000..1446ee1 --- /dev/null +++ b/labelme/widgets/deletetrack_dialog.py @@ -0,0 +1,86 @@ +from qtpy import QtWidgets + +class DeletionDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(DeletionDialog, self).__init__(parent) + self.setModal(True) + self.setWindowTitle("Modification Options") + + self.start_frame = -1 + self.end_frame = -1 + self.interval = -1 + self.ID = -1 + self.label = -1 + + self.start_frame_cell = QtWidgets.QLineEdit() + self.end_frame_cell = QtWidgets.QLineEdit() + self.interval_cell = QtWidgets.QLineEdit() + self.ID_cell = QtWidgets.QLineEdit() + self.label_cell = QtWidgets.QLineEdit() + self.new_ID_cell = QtWidgets.QLineEdit() + self.new_label_cell = QtWidgets.QLineEdit() + + self.row1 = QtWidgets.QHBoxLayout() + self.row1.addWidget(QtWidgets.QLabel("Start Frame:")) + self.row1.addStretch() + self.row1.addWidget(self.start_frame_cell) + + self.row2 = QtWidgets.QHBoxLayout() + self.row2.addWidget(QtWidgets.QLabel("End Frame:")) + self.row2.addStretch() + self.row2.addWidget(self.end_frame_cell) + + self.row4 = QtWidgets.QHBoxLayout() + self.row4.addWidget(QtWidgets.QLabel("Object ID:")) + self.row4.addStretch() + self.row4.addWidget(self.ID_cell) + + self.row5 = QtWidgets.QHBoxLayout() + self.row5.addWidget(QtWidgets.QLabel("Object Label")) + self.row5.addStretch() + self.row5.addWidget(self.label_cell) + + self.row6 = QtWidgets.QHBoxLayout() + self.row6.addWidget(QtWidgets.QLabel("New ID:")) + self.row6.addStretch() + self.row6.addWidget(self.new_ID_cell) + + self.row7 = QtWidgets.QHBoxLayout() + self.row7.addWidget(QtWidgets.QLabel("New Label")) + self.row7.addStretch() + self.row7.addWidget(self.new_label_cell) + + combobox = QtWidgets.QComboBox() + combobox.addItems(["Remove Box", "Swap Label", "Swap ID"]) + row8 = QtWidgets.QHBoxLayout() + row8.addWidget(QtWidgets.QLabel("Mode:")) + row8.addStretch() + row8.addWidget(combobox) + + self.button = QtWidgets.QPushButton("Finish") + self.button.clicked.connect(self.get_info) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(self.row1) + layout.addLayout(self.row2) + layout.addLayout(self.row4) + layout.addLayout(self.row5) + layout.addLayout(self.row6) + layout.addLayout(self.row7) + layout.addLayout(row8) + layout.addWidget(self.button) + self.setLayout(layout) + + def get_info(self): + self.start_frame = self.start_frame_cell.text() + self.end_frame = self.end_frame_cell.text() + self.ID = self.interval_cell.text() + self.label = self.label_cell.text() + + self.accept() + + + + + + diff --git a/labelme/widgets/escapable_qlist_widget.py b/labelme/widgets/escapable_qlist_widget.py new file mode 100644 index 0000000..5469344 --- /dev/null +++ b/labelme/widgets/escapable_qlist_widget.py @@ -0,0 +1,9 @@ +from qtpy import QtWidgets +from qtpy.QtCore import Qt + + +class EscapableQListWidget(QtWidgets.QListWidget): + def keyPressEvent(self, event): + super(EscapableQListWidget, self).keyPressEvent(event) + if event.key() == Qt.Key_Escape: + self.clearSelection() diff --git a/labelme/widgets/file_dialog_preview.py b/labelme/widgets/file_dialog_preview.py new file mode 100644 index 0000000..6110bcb --- /dev/null +++ b/labelme/widgets/file_dialog_preview.py @@ -0,0 +1,75 @@ +import json + +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + + +class ScrollAreaPreview(QtWidgets.QScrollArea): + def __init__(self, *args, **kwargs): + super(ScrollAreaPreview, self).__init__(*args, **kwargs) + + self.setWidgetResizable(True) + + content = QtWidgets.QWidget(self) + self.setWidget(content) + + lay = QtWidgets.QVBoxLayout(content) + + self.label = QtWidgets.QLabel(content) + self.label.setWordWrap(True) + + lay.addWidget(self.label) + + def setText(self, text): + self.label.setText(text) + + def setPixmap(self, pixmap): + self.label.setPixmap(pixmap) + + def clear(self): + self.label.clear() + + +class FileDialogPreview(QtWidgets.QFileDialog): + def __init__(self, *args, **kwargs): + super(FileDialogPreview, self).__init__(*args, **kwargs) + self.setOption(self.DontUseNativeDialog, True) + + self.labelPreview = ScrollAreaPreview(self) + self.labelPreview.setFixedSize(300, 300) + self.labelPreview.setHidden(True) + + box = QtWidgets.QVBoxLayout() + box.addWidget(self.labelPreview) + box.addStretch() + + self.setFixedSize(self.width() + 300, self.height()) + self.layout().addLayout(box, 1, 3, 1, 1) + self.currentChanged.connect(self.onChange) + + def onChange(self, path): + if path.lower().endswith(".json"): + with open(path, "r") as f: + data = json.load(f) + self.labelPreview.setText(json.dumps(data, indent=4, sort_keys=False)) + self.labelPreview.label.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop + ) + self.labelPreview.setHidden(False) + else: + pixmap = QtGui.QPixmap(path) + if pixmap.isNull(): + self.labelPreview.clear() + self.labelPreview.setHidden(True) + else: + self.labelPreview.setPixmap( + pixmap.scaled( + self.labelPreview.width() - 30, + self.labelPreview.height() - 30, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation, + ) + ) + self.labelPreview.label.setAlignment(QtCore.Qt.AlignCenter) + self.labelPreview.setHidden(False) diff --git a/labelme/widgets/id_dialog.py b/labelme/widgets/id_dialog.py new file mode 100644 index 0000000..e3f240a --- /dev/null +++ b/labelme/widgets/id_dialog.py @@ -0,0 +1,172 @@ +import re + +from qtpy import QT_VERSION +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +import labelme.utils +from labelme.logger import logger + +QT5 = QT_VERSION[0] == "5" + + +# TODO(unknown): +# - Calculate optimal position so as not to go out of screen area. + + +class IDQLineEdit(QtWidgets.QLineEdit): + def setListWidget(self, list_widget): + self.list_widget = list_widget + + def keyPressEvent(self, e): + if e.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]: + self.list_widget.keyPressEvent(e) + else: + super(IDQLineEdit, self).keyPressEvent(e) + + +class IDDialog(QtWidgets.QDialog): + def __init__( + self, + text="Enter object id", + parent=None, + ids=None, + sort_ids=True, + show_text_field=True, + completion="startswith", + fit_to_content=None + ): + if fit_to_content is None: + fit_to_content = {"row": False, "column": True} + self._fit_to_content = fit_to_content + + super(IDDialog, self).__init__(parent) + self.edit = IDQLineEdit() + self.edit.setPlaceholderText(text) + self.edit.setValidator(labelme.utils.labelValidator()) + self.edit.editingFinished.connect(self.postProcess) + + self.edit_track_id = QtWidgets.QLineEdit() + self.edit_track_id.setPlaceholderText("Track ID") + self.edit_track_id.setValidator( + QtGui.QRegExpValidator(QtCore.QRegExp(r"\d*"), None) + ) + layout = QtWidgets.QVBoxLayout() + if show_text_field: + layout_edit = QtWidgets.QHBoxLayout() + layout_edit.addWidget(self.edit, 6) + layout_edit.addWidget(self.edit_track_id, 2) + layout.addLayout(layout_edit) + # buttons + self.buttonBox = bb = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, + QtCore.Qt.Horizontal, + self, + ) + bb.button(bb.Ok).setIcon(labelme.utils.newIcon("done")) + bb.button(bb.Cancel).setIcon(labelme.utils.newIcon("undo")) + bb.accepted.connect(self.validate) + bb.rejected.connect(self.reject) + layout.addWidget(bb) + # ID_list + self.IDList = QtWidgets.QListWidget() + if self._fit_to_content["row"]: + self.IDList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + if self._fit_to_content["column"]: + self.IDList.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._sort_labels = sort_ids + if ids: + self.IDList.addItems(ids) + if self._sort_labels: + self.IDList.sortItems() + else: + self.IDList.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + self.IDList.currentItemChanged.connect(self.IDSelected) + self.IDList.itemDoubleClicked.connect(self.IDDoubleClicked) + self.IDList.setFixedHeight(150) + self.edit.setListWidget(self.IDList) + layout.addWidget(self.IDList) + self.setLayout(layout) + # completion + # completer = QtWidgets.QCompleter() + # if not QT5 and completion != "startswith": + # logger.warn( + # "completion other than 'startswith' is only " + # "supported with Qt5. Using 'startswith'" + # ) + # completion = "startswith" + # if completion == "startswith": + # completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion) + # # Default settings. + # # completer.setFilterMode(QtCore.Qt.MatchStartsWith) + # elif completion == "contains": + # completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) + # completer.setFilterMode(QtCore.Qt.MatchContains) + # else: + # raise ValueError("Unsupported completion: {}".format(completion)) + # completer.setModel(self.IDList.model()) + completer = QtWidgets.QCompleter(self.IDList.model(), self) + completer.activated.connect(self.popUp) + self.edit.setCompleter(completer) + + def addIDHistory(self, id): + if self.IDList.findItems(id,QtCore.Qt.MatchExactly): + return + self.IDList.addItem(id) + if self._sort_labels: + self.IDList.sortItems() + + def IDSelected(self, item): + self.edit.setText(item.text()) + + def validate(self): + text = self.edit.text() + if hasattr(text, "strip"): + text = text.strip() + else: + text = text.trimmed() + if text: + self.accept() + + def IDDoubleClicked(self,item): + self.validate() + + def postProcess(self): + text = self.edit.text() + if hasattr(text, "strip"): + text = text.strip() + else: + text = text.trimmed() + self.edit.setText(text) + + def popUp(self, text=None, move=True): + if self._fit_to_content["row"]: + self.IDList.setMinimumHeight( + self.IDList.sizeHintForRow(0) * self.IDList.count() + 2 + ) + if self._fit_to_content["column"]: + self.IDList.setMinimumWidth(self.IDList.sizeHintForColumn(0) + 2) + # if text is None, the previous label in self.edit is kept + if text is None: + text = self.edit.text() + self.edit.setText(text) + self.edit.setSelection(0, len(text)) + + items = self.IDList.findItems(text, QtCore.Qt.MatchFixedString) + if items: + if len(items) != 1: + logger.warning("ID list has duplicate '{}'".format(text)) + self.IDList.setCurrentItem(items[0]) + row = self.IDList.row(items[0]) + self.edit.completer().setCurrentRow(row) + + self.edit.setFocus(QtCore.Qt.PopupFocusReason) + if move: + self.move(QtGui.QCursor.pos()) + if self.exec_(): + return ( + self.edit.text() + ) + else: + return None diff --git a/labelme/widgets/id_list_widget.py b/labelme/widgets/id_list_widget.py new file mode 100644 index 0000000..5d24cdf --- /dev/null +++ b/labelme/widgets/id_list_widget.py @@ -0,0 +1,176 @@ +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtGui import QPalette +from qtpy.QtWidgets import QStyle + + +# https://stackoverflow.com/a/2039745/4158863 +class HTMLDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, parent=None): + super(HTMLDelegate, self).__init__() + self.doc = QtGui.QTextDocument(self) + + def paint(self, painter, option, index): + painter.save() + + options = QtWidgets.QStyleOptionViewItem(option) + + self.initStyleOption(options, index) + self.doc.setHtml(options.text) + options.text = "" + + style = ( + QtWidgets.QApplication.style() + if options.widget is None + else options.widget.style() + ) + style.drawControl(QStyle.CE_ItemViewItem, options, painter) + + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() + + if option.state & QStyle.State_Selected: + ctx.palette.setColor( + QPalette.Text, + option.palette.color(QPalette.Active, QPalette.HighlightedText), + ) + else: + ctx.palette.setColor( + QPalette.Text, + option.palette.color(QPalette.Active, QPalette.Text), + ) + + textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) + + if index.column() != 0: + textRect.adjust(5, 0, 0, 0) + + thefuckyourshitup_constant = 4 + margin = (option.rect.height() - options.fontMetrics.height()) // 2 + margin = margin - thefuckyourshitup_constant + textRect.setTop(textRect.top() + margin) + + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + self.doc.documentLayout().draw(painter, ctx) + painter.restore() + + def sizeHint(self, option, index): + thefuckyourshitup_constant = 4 + return QtCore.QSize( + int(self.doc.idealWidth()), + int(self.doc.size().height() - thefuckyourshitup_constant), + ) + + +class IDListWidgetItem(QtGui.QStandardItem): + def __init__(self, text=None, shape=None): + super(IDListWidgetItem, self).__init__() + self.setText(text or "") + self.setShape(shape) + + self.setCheckable(True) + self.setCheckState(Qt.Checked) + self.setEditable(False) + self.setTextAlignment(Qt.AlignBottom) + + def clone(self): + return IDListWidgetItem(self.text(), self.shape()) + + def setShape(self, shape): + self.setData(shape, Qt.UserRole) + + def shape(self): + return self.data(Qt.UserRole) + + def __hash__(self): + return id(self) + + def __repr__(self): + return '{}("{}")'.format(self.__class__.__name__, self.text()) + + +class StandardItemModel(QtGui.QStandardItemModel): + itemDropped = QtCore.Signal() + + def removeRows(self, *args, **kwargs): + ret = super().removeRows(*args, **kwargs) + self.itemDropped.emit() + return ret + + +class IDListWidget(QtWidgets.QListView): + itemDoubleClicked = QtCore.Signal(IDListWidgetItem) + itemSelectionChanged = QtCore.Signal(list, list) + + def __init__(self): + super(IDListWidget, self).__init__() + self._selectedItems = [] + + self.setWindowFlags(Qt.Window) + self.setModel(StandardItemModel()) + self.model().setItemPrototype(IDListWidgetItem()) + self.setItemDelegate(HTMLDelegate()) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + self.setDefaultDropAction(Qt.MoveAction) + + self.doubleClicked.connect(self.itemDoubleClickedEvent) + self.selectionModel().selectionChanged.connect(self.itemSelectionChangedEvent) + + def __len__(self): + return self.model().rowCount() + + def __getitem__(self, i): + return self.model().item(i) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + @property + def itemDropped(self): + return self.model().itemDropped + + @property + def itemChanged(self): + return self.model().itemChanged + + def itemSelectionChangedEvent(self, selected, deselected): + selected = [self.model().itemFromIndex(i) for i in selected.indexes()] + deselected = [self.model().itemFromIndex(i) for i in deselected.indexes()] + self.itemSelectionChanged.emit(selected, deselected) + + def itemDoubleClickedEvent(self, index): + self.itemDoubleClicked.emit(self.model().itemFromIndex(index)) + + def selectedItems(self): + return [self.model().itemFromIndex(i) for i in self.selectedIndexes()] + + def scrollToItem(self, item): + self.scrollTo(self.model().indexFromItem(item)) + + def addItem(self, item): + if not isinstance(item, IDListWidgetItem): + raise TypeError("item must be IDListWidgetItem") + self.model().setItem(self.model().rowCount(), 0, item) + item.setSizeHint(self.itemDelegate().sizeHint(None, None)) + + def removeItem(self, item): + index = self.model().indexFromItem(item) + self.model().removeRows(index.row(), 1) + + def selectItem(self, item): + index = self.model().indexFromItem(item) + self.selectionModel().select(index, QtCore.QItemSelectionModel.Select) + + def findItemByShape(self, shape): + for row in range(self.model().rowCount()): + item = self.model().item(row, 0) + if item.shape() == shape: + return item + raise ValueError("cannot find shape: {}".format(shape)) + + def clear(self): + self.model().clear() \ No newline at end of file diff --git a/labelme/widgets/interpolation_dialog.py b/labelme/widgets/interpolation_dialog.py new file mode 100644 index 0000000..4d9fa77 --- /dev/null +++ b/labelme/widgets/interpolation_dialog.py @@ -0,0 +1,110 @@ +from qtpy import QtWidgets +from qtpy.QtCore import Qt + +class InterpolationDialog(QtWidgets.QDialog): + def __init__(self, min_val, max_val, parent=None): + super(InterpolationDialog, self).__init__(parent) + self.setModal(True) + self.setWindowTitle("Interpolation Options") + + self.start_frame = -1 + self.end_frame = -1 + self.interval = -1 + self.ID = -1 + self.label = -1 + + self.start_value = 0 + self.end_value = 0 + + self.start_frame_cell = QtWidgets.QLineEdit() + self.end_frame_cell = QtWidgets.QLineEdit() + self.interval_cell = QtWidgets.QLineEdit() + self.ID_cell = QtWidgets.QLineEdit() + self.label_cell = QtWidgets.QLineEdit() + + row1 = QtWidgets.QHBoxLayout() + row1.addWidget(QtWidgets.QLabel("Start Frame:")) + row1.addStretch() + row1.addWidget(self.start_frame_cell) + + row2 = QtWidgets.QHBoxLayout() + row2.addWidget(QtWidgets.QLabel("End Frame:")) + row2.addStretch() + row2.addWidget(self.end_frame_cell) + + row3 = QtWidgets.QHBoxLayout() + row3.addWidget(QtWidgets.QLabel("Interval/FPS:")) + row3.addStretch() + row3.addWidget(self.interval_cell) + + row4 = QtWidgets.QHBoxLayout() + row4.addWidget(QtWidgets.QLabel("Object ID:")) + row4.addStretch() + row4.addWidget(self.ID_cell) + + row5 = QtWidgets.QHBoxLayout() + row5.addWidget(QtWidgets.QLabel("Object Label")) + row5.addStretch() + row5.addWidget(self.label_cell) + + # startsliderLayout = QtWidgets.QHBoxLayout() + # self.start_slider = QtWidgets.QSlider(Qt.Horizontal) + # self.start_slider.setPageStep(1) + # self.start_slider.setRange(min_val, max_val) + # self.start_slider.setValue(min_val) + # self.start_slider.valueChanged.connect(self.onNewValue) + # self.start_label = QtWidgets.QLabel(str(min_val), self) + # startsliderLayout.addWidget(QtWidgets.QLabel("Start Frame:")) + # startsliderLayout.addWidget(self.start_slider) + # startsliderLayout.addWidget(self.start_label) + + # endsliderLayout = QtWidgets.QHBoxLayout() + # self.end_slider = QtWidgets.QSlider(Qt.Horizontal) + # self.end_slider.setPageStep(1) + # self.end_slider.setRange(min_val, max_val) + # self.end_slider.setValue(max_val) + # self.end_slider.valueChanged.connect(self.onNewValue) + # self.end_label = QtWidgets.QLabel(str(max_val), self) + # endsliderLayout.addWidget(QtWidgets.QLabel("End Frame:")) + # endsliderLayout.addWidget(self.end_slider) + # endsliderLayout.addWidget(self.end_label) + + + # self.formLayout = QtWidgets.QFormLayout() + # self.formLayout.addRow(self.tr("Start Frame:"), self.slider_start_frame) + # self.formLayout.addRow(self.tr("End Frame:"), self.slider_end_frame) + + self.button = QtWidgets.QPushButton("Finish") + self.button.clicked.connect(self.get_info) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(row1) + layout.addLayout(row2) + layout.addLayout(row3) + layout.addLayout(row4) + layout.addLayout(row5) + # layout.addLayout(startsliderLayout,stretch=3) + # layout.addLayout(endsliderLayout,stretch=3) + layout.addWidget(self.button) + self.setLayout(layout) + + def get_info(self): + self.start_frame = self.start_frame_cell.text() + self.end_frame = self.end_frame_cell.text() + self.interval = self.interval_cell.text() + self.ID = self.interval_cell.text() + self.label = self.label_cell.text() + + self.accept() + + # def onNewValue(self): + # start_value = self.start_slider.value() + # end_value = self.end_slider.value() + # self.start_label.setText(str(start_value)) + # self.end_label.setText(str(end_value)) + + + + + + diff --git a/labelme/widgets/interpolationrefine_widget.py b/labelme/widgets/interpolationrefine_widget.py new file mode 100644 index 0000000..94ca48e --- /dev/null +++ b/labelme/widgets/interpolationrefine_widget.py @@ -0,0 +1,36 @@ +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy import QtCore + +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtGui import QPalette +from qtpy.QtWidgets import QStyle + +import labelme.utils +from labelme.logger import logger + +from .. import utils + + +class IterpolationRefineWidget(QtWidgets.QDialog): + def __init__(self, parent=None): + super(IterpolationRefineWidget, self).__init__(parent) + + self.button = QtWidgets.QPushButton("Edit") + + self.statusBar = QtWidgets.QStatusBar() + self.statusBar.setStyleSheet("border :1px solid black;border-radius: 1px ;text-align: center; ") + self.statusBar.showMessage('Name: None | ID: None') + + self.checkBox = QtWidgets.QCheckBox() + + navigationLayout = QtWidgets.QHBoxLayout() + navigationLayout.addWidget(self.statusBar) + navigationLayout.addWidget(self.checkBox ) + navigationLayout.addWidget(self.button ) + + self.setLayout(navigationLayout) \ No newline at end of file diff --git a/labelme/widgets/interpolationrefineinfo_dialog.py b/labelme/widgets/interpolationrefineinfo_dialog.py new file mode 100644 index 0000000..bd6ece6 --- /dev/null +++ b/labelme/widgets/interpolationrefineinfo_dialog.py @@ -0,0 +1,39 @@ +from qtpy import QtWidgets + +class InterpolationRefineInfo_Dialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(InterpolationRefineInfo_Dialog, self).__init__(parent) + self.setModal(True) + self.setWindowTitle("Association Options") + self.name = "None" + self.ID = "None" + + self.name_cell = QtWidgets.QLineEdit() + self.id_cell = QtWidgets.QLineEdit() + + row1 = QtWidgets.QHBoxLayout() + row1.addWidget(QtWidgets.QLabel("Name:")) + row1.addStretch() + row1.addWidget(self.name_cell) + + row2 = QtWidgets.QHBoxLayout() + row2.addWidget(QtWidgets.QLabel("ID:")) + row2.addStretch() + row2.addWidget(self.id_cell) + + self.button = QtWidgets.QPushButton("Finish") + self.button.clicked.connect(self.get_info) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(row1) + layout.addLayout(row2) + layout.addWidget(self.button) + self.setLayout(layout) + + def get_info(self): + self.name = self.name_cell.text() + self.id = self.id_cell.text() + + self.accept() + + diff --git a/labelme/widgets/label_dialog.py b/labelme/widgets/label_dialog.py new file mode 100644 index 0000000..9db6be5 --- /dev/null +++ b/labelme/widgets/label_dialog.py @@ -0,0 +1,248 @@ +import re + +from qtpy import QT_VERSION +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + +import labelme.utils +from labelme.logger import logger + +QT5 = QT_VERSION[0] == "5" + + +# TODO(unknown): +# - Calculate optimal position so as not to go out of screen area. + + +class LabelQLineEdit(QtWidgets.QLineEdit): + def setListWidget(self, list_widget): + self.list_widget = list_widget + + def keyPressEvent(self, e): + if e.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]: + self.list_widget.keyPressEvent(e) + else: + super(LabelQLineEdit, self).keyPressEvent(e) + + +class LabelDialog(QtWidgets.QDialog): + def __init__( + self, + text="Enter object label", + parent=None, + labels=None, + sort_labels=True, + show_text_field=True, + completion="startswith", + fit_to_content=None, + flags=None, + ): + if fit_to_content is None: + fit_to_content = {"row": False, "column": True} + self._fit_to_content = fit_to_content + + super(LabelDialog, self).__init__(parent) + self.edit = LabelQLineEdit() + self.edit.setPlaceholderText(text) + self.edit.setValidator(labelme.utils.labelValidator()) + self.edit.editingFinished.connect(self.postProcess) + if flags: + self.edit.textChanged.connect(self.updateFlags) + self.edit_group_id = QtWidgets.QLineEdit() + self.edit_group_id.setPlaceholderText("Group ID") + self.edit_group_id.setValidator( + QtGui.QRegExpValidator(QtCore.QRegExp(r"\d*"), None) + ) + layout = QtWidgets.QVBoxLayout() + if show_text_field: + layout_edit = QtWidgets.QHBoxLayout() + layout_edit.addWidget(self.edit, 6) + layout_edit.addWidget(self.edit_group_id, 2) + layout.addLayout(layout_edit) + # buttons + self.buttonBox = bb = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, + QtCore.Qt.Horizontal, + self, + ) + bb.button(bb.Ok).setIcon(labelme.utils.newIcon("done")) + bb.button(bb.Cancel).setIcon(labelme.utils.newIcon("undo")) + bb.accepted.connect(self.validate) + bb.rejected.connect(self.reject) + layout.addWidget(bb) + # label_list + self.labelList = QtWidgets.QListWidget() + if self._fit_to_content["row"]: + self.labelList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + if self._fit_to_content["column"]: + self.labelList.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self._sort_labels = sort_labels + if labels: + self.labelList.addItems(labels) + if self._sort_labels: + self.labelList.sortItems() + else: + self.labelList.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + self.labelList.currentItemChanged.connect(self.labelSelected) + self.labelList.itemDoubleClicked.connect(self.labelDoubleClicked) + self.labelList.setFixedHeight(150) + self.edit.setListWidget(self.labelList) + layout.addWidget(self.labelList) + # label_flags + if flags is None: + flags = {} + self._flags = flags + self.flagsLayout = QtWidgets.QVBoxLayout() + self.resetFlags() + layout.addItem(self.flagsLayout) + self.edit.textChanged.connect(self.updateFlags) + # text edit + self.editDescription = QtWidgets.QTextEdit() + self.editDescription.setPlaceholderText("Label description") + self.editDescription.setFixedHeight(50) + layout.addWidget(self.editDescription) + self.setLayout(layout) + # completion + # completer = QtWidgets.QCompleter() + # if not QT5 and completion != "startswith": + # logger.warn( + # "completion other than 'startswith' is only " + # "supported with Qt5. Using 'startswith'" + # ) + # completion = "startswith" + # if completion == "startswith": + # completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion) + # # Default settings. + # # completer.setFilterMode(QtCore.Qt.MatchStartsWith) + # elif completion == "contains": + # completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) + # completer.setFilterMode(QtCore.Qt.MatchContains) + # else: + # raise ValueError("Unsupported completion: {}".format(completion)) + # completer.setModel(self.labelList.model()) + # self.edit.setCompleter(completer) + completer = QtWidgets.QCompleter(self.labelList.model(), self) + completer.activated.connect(self.popUp) + self.edit.setCompleter(completer) + + def addLabelHistory(self, label): + if self.labelList.findItems(label, QtCore.Qt.MatchExactly): + return + self.labelList.addItem(label) + if self._sort_labels: + self.labelList.sortItems() + + def labelSelected(self, item): + self.edit.setText(item.text()) + + def validate(self): + text = self.edit.text() + if hasattr(text, "strip"): + text = text.strip() + else: + text = text.trimmed() + if text: + self.accept() + + def labelDoubleClicked(self, item): + self.validate() + + def postProcess(self): + text = self.edit.text() + if hasattr(text, "strip"): + text = text.strip() + else: + text = text.trimmed() + self.edit.setText(text) + + def updateFlags(self, label_new): + # keep state of shared flags + flags_old = self.getFlags() + + flags_new = {} + for pattern, keys in self._flags.items(): + if re.match(pattern, label_new): + for key in keys: + flags_new[key] = flags_old.get(key, False) + self.setFlags(flags_new) + + def deleteFlags(self): + for i in reversed(range(self.flagsLayout.count())): + item = self.flagsLayout.itemAt(i).widget() + self.flagsLayout.removeWidget(item) + item.setParent(None) + + def resetFlags(self, label=""): + flags = {} + + for pattern, keys in self._flags.items(): + if re.match(pattern, label): + for key in keys: + flags[key] = False + self.setFlags(flags) + + def setFlags(self, flags): + self.deleteFlags() + for key in flags: + item = QtWidgets.QCheckBox(key, self) + item.setChecked(flags[key]) + self.flagsLayout.addWidget(item) + item.show() + + def getFlags(self): + flags = {} + for i in range(self.flagsLayout.count()): + item = self.flagsLayout.itemAt(i).widget() + flags[item.text()] = item.isChecked() + return flags + + def getGroupId(self): + group_id = self.edit_group_id.text() + if group_id: + return int(group_id) + return None + + def popUp(self, text=None, move=True, flags=None, group_id=None, description=None): + if self._fit_to_content["row"]: + self.labelList.setMinimumHeight( + self.labelList.sizeHintForRow(0) * self.labelList.count() + 2 + ) + if self._fit_to_content["column"]: + self.labelList.setMinimumWidth(self.labelList.sizeHintForColumn(0) + 2) + # if text is None, the previous label in self.edit is kept + if text is None: + text = self.edit.text() + # description is always initialized by empty text c.f., self.edit.text + if description is None: + description = "" + self.editDescription.setPlainText(description) + if flags: + self.setFlags(flags) + else: + self.resetFlags(text) + self.edit.setText(text) + self.edit.setSelection(0, len(text)) + if group_id is None: + self.edit_group_id.clear() + else: + self.edit_group_id.setText(str(group_id)) + items = self.labelList.findItems(text, QtCore.Qt.MatchFixedString) + if items: + if len(items) != 1: + logger.warning("Label list has duplicate '{}'".format(text)) + self.labelList.setCurrentItem(items[0]) + row = self.labelList.row(items[0]) + self.edit.completer().setCurrentRow(row) + self.edit.setFocus(QtCore.Qt.PopupFocusReason) + if move: + self.move(QtGui.QCursor.pos()) + if self.exec_(): + return ( + self.edit.text(), + self.getFlags(), + self.getGroupId(), + self.editDescription.toPlainText(), + ) + else: + return None, None, None, None diff --git a/labelme/widgets/label_list_widget.py b/labelme/widgets/label_list_widget.py new file mode 100644 index 0000000..9af4711 --- /dev/null +++ b/labelme/widgets/label_list_widget.py @@ -0,0 +1,176 @@ +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtGui import QPalette +from qtpy.QtWidgets import QStyle + + +# https://stackoverflow.com/a/2039745/4158863 +class HTMLDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, parent=None): + super(HTMLDelegate, self).__init__() + self.doc = QtGui.QTextDocument(self) + + def paint(self, painter, option, index): + painter.save() + + options = QtWidgets.QStyleOptionViewItem(option) + + self.initStyleOption(options, index) + self.doc.setHtml(options.text) + options.text = "" + + style = ( + QtWidgets.QApplication.style() + if options.widget is None + else options.widget.style() + ) + style.drawControl(QStyle.CE_ItemViewItem, options, painter) + + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() + + if option.state & QStyle.State_Selected: + ctx.palette.setColor( + QPalette.Text, + option.palette.color(QPalette.Active, QPalette.HighlightedText), + ) + else: + ctx.palette.setColor( + QPalette.Text, + option.palette.color(QPalette.Active, QPalette.Text), + ) + + textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) + + if index.column() != 0: + textRect.adjust(5, 0, 0, 0) + + thefuckyourshitup_constant = 4 + margin = (option.rect.height() - options.fontMetrics.height()) // 2 + margin = margin - thefuckyourshitup_constant + textRect.setTop(textRect.top() + margin) + + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + self.doc.documentLayout().draw(painter, ctx) + painter.restore() + + def sizeHint(self, option, index): + thefuckyourshitup_constant = 4 + return QtCore.QSize( + int(self.doc.idealWidth()), + int(self.doc.size().height() - thefuckyourshitup_constant), + ) + + +class LabelListWidgetItem(QtGui.QStandardItem): + def __init__(self, text=None, shape=None): + super(LabelListWidgetItem, self).__init__() + self.setText(text or "") + self.setShape(shape) + + self.setCheckable(True) + self.setCheckState(Qt.Checked) + self.setEditable(False) + self.setTextAlignment(Qt.AlignBottom) + + def clone(self): + return LabelListWidgetItem(self.text(), self.shape()) + + def setShape(self, shape): + self.setData(shape, Qt.UserRole) + + def shape(self): + return self.data(Qt.UserRole) + + def __hash__(self): + return id(self) + + def __repr__(self): + return '{}("{}")'.format(self.__class__.__name__, self.text()) + + +class StandardItemModel(QtGui.QStandardItemModel): + itemDropped = QtCore.Signal() + + def removeRows(self, *args, **kwargs): + ret = super().removeRows(*args, **kwargs) + self.itemDropped.emit() + return ret + + +class LabelListWidget(QtWidgets.QListView): + itemDoubleClicked = QtCore.Signal(LabelListWidgetItem) + itemSelectionChanged = QtCore.Signal(list, list) + + def __init__(self): + super(LabelListWidget, self).__init__() + self._selectedItems = [] + + self.setWindowFlags(Qt.Window) + self.setModel(StandardItemModel()) + self.model().setItemPrototype(LabelListWidgetItem()) + self.setItemDelegate(HTMLDelegate()) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) + self.setDefaultDropAction(Qt.MoveAction) + + self.doubleClicked.connect(self.itemDoubleClickedEvent) + self.selectionModel().selectionChanged.connect(self.itemSelectionChangedEvent) + + def __len__(self): + return self.model().rowCount() + + def __getitem__(self, i): + return self.model().item(i) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + @property + def itemDropped(self): + return self.model().itemDropped + + @property + def itemChanged(self): + return self.model().itemChanged + + def itemSelectionChangedEvent(self, selected, deselected): + selected = [self.model().itemFromIndex(i) for i in selected.indexes()] + deselected = [self.model().itemFromIndex(i) for i in deselected.indexes()] + self.itemSelectionChanged.emit(selected, deselected) + + def itemDoubleClickedEvent(self, index): + self.itemDoubleClicked.emit(self.model().itemFromIndex(index)) + + def selectedItems(self): + return [self.model().itemFromIndex(i) for i in self.selectedIndexes()] + + def scrollToItem(self, item): + self.scrollTo(self.model().indexFromItem(item)) + + def addItem(self, item): + if not isinstance(item, LabelListWidgetItem): + raise TypeError("item must be LabelListWidgetItem") + self.model().setItem(self.model().rowCount(), 0, item) + item.setSizeHint(self.itemDelegate().sizeHint(None, None)) + + def removeItem(self, item): + index = self.model().indexFromItem(item) + self.model().removeRows(index.row(), 1) + + def selectItem(self, item): + index = self.model().indexFromItem(item) + self.selectionModel().select(index, QtCore.QItemSelectionModel.Select) + + def findItemByShape(self, shape): + for row in range(self.model().rowCount()): + item = self.model().item(row, 0) + if item.shape() == shape: + return item + raise ValueError("cannot find shape: {}".format(shape)) + + def clear(self): + self.model().clear() diff --git a/labelme/widgets/navigation_widget.py b/labelme/widgets/navigation_widget.py new file mode 100644 index 0000000..260cb4e --- /dev/null +++ b/labelme/widgets/navigation_widget.py @@ -0,0 +1,81 @@ +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy import QtCore + +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtGui import QPalette +from qtpy.QtWidgets import QStyle + +import labelme.utils +from labelme.logger import logger + +from .. import utils + +""" +class NavigationWidget(QtWidgets.QDialogButtonBox): + def __init__(self, parent=None): + super(NavigationWidget, self).__init__(parent) + self.setOrientation(Qt.Horizontal) + self.button1 = QtWidgets.QPushButton("Next Image (D)") + # self.button1.clicked.connect(self.next) + self.addButton(self.button1, QtWidgets.QDialogButtonBox.ActionRole) + self.button2 = QtWidgets.QPushButton("Previous Image (A)") + # self.button2.clicked.connect(self.prev) + self.addButton(self.button2, QtWidgets.QDialogButtonBox.ActionRole) + self.button3 = QtWidgets.QPushButton("OK") + # self.button3.clicked.connect(self.okay) + self.addButton(self.button3, QtWidgets.QDialogButtonBox.ActionRole) + + self.statusBar = QtWidgets.QStatusBar() + self.statusBar.showMessage('Ready') + + + # buttonLayout = QtWidgets.QHBoxLayout() + # buttonLayout.addWidget(self.button1) + # buttonLayout.addWidget(self.button2) + # buttonLayout.addWidget(self.button3) + # self.setLayout(buttonLayout) + + # self.option_value = 0 + + # def next(self): + # self.option_value = 1 + # print("1") + # self.accept() + + # def prev(self): + # self.option_value = 2 + # print("2") + # self.accept() + + # def okay(self): + # self.option_value = 3 + # print("3") + # self.accept() +""" + +class NavigationWidget(QtWidgets.QDialog): + def __init__(self, parent=None): + super(NavigationWidget, self).__init__(parent) + + self.button_box = QtWidgets.QDialogButtonBox() + self.button_box.setOrientation(Qt.Horizontal) + self.button1 = QtWidgets.QPushButton("FINISH") + self.button_box.addButton(self.button1, QtWidgets.QDialogButtonBox.ActionRole) + self.button2 = QtWidgets.QPushButton("Next Image (D)") + self.button_box.addButton(self.button2, QtWidgets.QDialogButtonBox.ActionRole) + self.button3 = QtWidgets.QPushButton("Previous Image (A)") + self.button_box.addButton(self.button3, QtWidgets.QDialogButtonBox.ActionRole) + + self.statusBar = QtWidgets.QStatusBar() + self.statusBar.setStyleSheet("border :1px solid black;border-radius: 1px ;text-align: center; ") + self.statusBar.showMessage('Status: Not Ready | Mode: None') + + navigationLayout = QtWidgets.QVBoxLayout() + navigationLayout.addWidget(self.button_box) + navigationLayout.addWidget(self.statusBar) + self.setLayout(navigationLayout) \ No newline at end of file diff --git a/labelme/widgets/tool_bar.py b/labelme/widgets/tool_bar.py new file mode 100644 index 0000000..a008724 --- /dev/null +++ b/labelme/widgets/tool_bar.py @@ -0,0 +1,26 @@ +from qtpy import QtCore +from qtpy import QtWidgets + + +class ToolBar(QtWidgets.QToolBar): + def __init__(self, title): + super(ToolBar, self).__init__(title) + layout = self.layout() + m = (0, 0, 0, 0) + layout.setSpacing(0) + layout.setContentsMargins(*m) + self.setContentsMargins(*m) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) + + def addAction(self, action): + if isinstance(action, QtWidgets.QWidgetAction): + return super(ToolBar, self).addAction(action) + btn = QtWidgets.QToolButton() + btn.setDefaultAction(action) + btn.setToolButtonStyle(self.toolButtonStyle()) + self.addWidget(btn) + + # center align + for i in range(self.layout().count()): + if isinstance(self.layout().itemAt(i).widget(), QtWidgets.QToolButton): + self.layout().itemAt(i).setAlignment(QtCore.Qt.AlignCenter) diff --git a/labelme/widgets/track_dialog.py b/labelme/widgets/track_dialog.py new file mode 100644 index 0000000..84c0648 --- /dev/null +++ b/labelme/widgets/track_dialog.py @@ -0,0 +1,45 @@ +from qtpy import QtWidgets + +class TrackDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(TrackDialog, self).__init__(parent) + self.setModal(True) + self.setWindowTitle("Association Options") + + self.button1 = QtWidgets.QPushButton("Track from Scratch") + self.button1.clicked.connect(self.option1) + self.button2 = QtWidgets.QPushButton("Track w/ Current Annotation") + self.button2.clicked.connect(self.option2) + + self.end_frame = QtWidgets.QLineEdit() + row1 = QtWidgets.QHBoxLayout() + row1.addWidget(QtWidgets.QLabel("End Frame:")) + row1.addStretch() + row1.addWidget(self.end_frame) + + combobox = QtWidgets.QComboBox() + combobox.addItems(["SORT", "Byte-SORT", "OC-SORT"]) + row2 = QtWidgets.QHBoxLayout() + row2.addWidget(QtWidgets.QLabel("Methods:")) + row2.addStretch() + row2.addWidget(combobox) + + buttonLayout = QtWidgets.QVBoxLayout() + buttonLayout.addWidget(self.button1) + buttonLayout.addWidget(self.button2) + buttonLayout.addLayout(row1) + buttonLayout.addLayout(row2) + self.setLayout(buttonLayout) + + self.option_value = 0 + + def option1(self): + self.option_value = 1 + self.accept() + + def option2(self): + self.option_value = 2 + self.accept() + + + diff --git a/labelme/widgets/unique_label_qlist_widget.py b/labelme/widgets/unique_label_qlist_widget.py new file mode 100644 index 0000000..19ef748 --- /dev/null +++ b/labelme/widgets/unique_label_qlist_widget.py @@ -0,0 +1,45 @@ +# -*- encoding: utf-8 -*- + +import html + +from qtpy import QtWidgets +from qtpy.QtCore import Qt + +from .escapable_qlist_widget import EscapableQListWidget + + +class UniqueLabelQListWidget(EscapableQListWidget): + def mousePressEvent(self, event): + super(UniqueLabelQListWidget, self).mousePressEvent(event) + if not self.indexAt(event.pos()).isValid(): + self.clearSelection() + + def findItemByLabel(self, label): + for row in range(self.count()): + item = self.item(row) + if item.data(Qt.UserRole) == label: + return item + + def createItemFromLabel(self, label): + if self.findItemByLabel(label): + raise ValueError("Item for label '{}' already exists".format(label)) + + item = QtWidgets.QListWidgetItem() + item.setData(Qt.UserRole, label) + return item + + def setItemLabel(self, item, label, color=None): + qlabel = QtWidgets.QLabel() + if color is None: + qlabel.setText("{}".format(label)) + else: + qlabel.setText( + '{} '.format( + html.escape(label), *color + ) + ) + qlabel.setAlignment(Qt.AlignBottom) + + item.setSizeHint(qlabel.sizeHint()) + + self.setItemWidget(item, qlabel) diff --git a/labelme/widgets/zoom_widget.py b/labelme/widgets/zoom_widget.py new file mode 100644 index 0000000..13fb2c2 --- /dev/null +++ b/labelme/widgets/zoom_widget.py @@ -0,0 +1,21 @@ +from qtpy import QtCore +from qtpy import QtGui +from qtpy import QtWidgets + + +class ZoomWidget(QtWidgets.QSpinBox): + def __init__(self, value=100): + super(ZoomWidget, self).__init__() + self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.setRange(1, 1000) + self.setSuffix(" %") + self.setValue(value) + self.setToolTip("Zoom Level") + self.setStatusTip(self.toolTip()) + self.setAlignment(QtCore.Qt.AlignCenter) + + def minimumSizeHint(self): + height = super(ZoomWidget, self).minimumSizeHint().height() + fm = QtGui.QFontMetrics(self.font()) + width = fm.width(str(self.maximum())) + return QtCore.QSize(width, height) diff --git a/old_README.md b/old_README.md new file mode 100644 index 0000000..b357678 --- /dev/null +++ b/old_README.md @@ -0,0 +1,221 @@ +

+
labelme +

+ +

+ Image Polygonal Annotation with Python +

+ +
+ + + +
+ + + +
+ +
+ +
+ +## Description + +Labelme is a graphical image annotation tool inspired by . +It is written in Python and uses Qt for its graphical interface. + + +VOC dataset example of instance segmentation. + + +Other examples (semantic segmentation, bbox detection, and classification). + + +Various primitives (polygon, rectangle, circle, line, and point). + + +## Features + +- [x] Image annotation for polygon, rectangle, circle, line and point. ([tutorial](examples/tutorial)) +- [x] Image flag annotation for classification and cleaning. ([#166](https://github.com/wkentaro/labelme/pull/166)) +- [x] Video annotation. ([video annotation](examples/video_annotation)) +- [x] GUI customization (predefined labels / flags, auto-saving, label validation, etc). ([#144](https://github.com/wkentaro/labelme/pull/144)) +- [x] Exporting VOC-format dataset for semantic/instance segmentation. ([semantic segmentation](examples/semantic_segmentation), [instance segmentation](examples/instance_segmentation)) +- [x] Exporting COCO-format dataset for instance segmentation. ([instance segmentation](examples/instance_segmentation)) + + +## Starter Guide + +If you're new to Labelme, you can get started with [Labelme Starter Guide](https://labelme.gumroad.com/l/starter-guide) (FREE), which contains: + +- **Installation guides** for all platforms: Windows, macOS, and Linux 💻 +- **Step-by-step tutorials**: first annotation to editing, exporting, and integrating with other programs 📕 +- **A compilation of valuable resources** for further exploration 🔗. + + +## Installation + +There are options: + +- Platform agnostic installation: [Anaconda](#anaconda) +- Platform specific installation: [Ubuntu](#ubuntu), [macOS](#macos), [Windows](#windows) +- Pre-build binaries from [the release section](https://github.com/wkentaro/labelme/releases) + +### Anaconda + +You need install [Anaconda](https://www.continuum.io/downloads), then run below: + +```bash +# python3 +conda create --name=labelme python=3 +source activate labelme +# conda install -c conda-forge pyside2 +# conda install pyqt +# pip install pyqt5 # pyqt5 can be installed via pip on python3 +pip install labelme +# or you can install everything by conda command +# conda install labelme -c conda-forge +``` + +### Ubuntu + +```bash +sudo apt-get install labelme + +# or +sudo pip3 install labelme + +# or install standalone executable from: +# https://github.com/wkentaro/labelme/releases +``` + +### macOS + +```bash +brew install pyqt # maybe pyqt5 +pip install labelme + +# or +brew install wkentaro/labelme/labelme # command line interface +# brew install --cask wkentaro/labelme/labelme # app + +# or install standalone executable/app from: +# https://github.com/wkentaro/labelme/releases +``` + +### Windows + +Install [Anaconda](https://www.continuum.io/downloads), then in an Anaconda Prompt run: + +```bash +conda create --name=labelme python=3 +conda activate labelme +pip install labelme + +# or install standalone executable/app from: +# https://github.com/wkentaro/labelme/releases +``` + + +## Usage + +Run `labelme --help` for detail. +The annotations are saved as a [JSON](http://www.json.org/) file. + +```bash +labelme # just open gui + +# tutorial (single image example) +cd examples/tutorial +labelme apc2016_obj3.jpg # specify image file +labelme apc2016_obj3.jpg -O apc2016_obj3.json # close window after the save +labelme apc2016_obj3.jpg --nodata # not include image data but relative image path in JSON file +labelme apc2016_obj3.jpg \ + --labels highland_6539_self_stick_notes,mead_index_cards,kong_air_dog_squeakair_tennis_ball # specify label list + +# semantic segmentation example +cd examples/semantic_segmentation +labelme data_annotated/ # Open directory to annotate all images in it +labelme data_annotated/ --labels labels.txt # specify label list with a file +``` + +### Command Line Arguments +- `--output` specifies the location that annotations will be written to. If the location ends with .json, a single annotation will be written to this file. Only one image can be annotated if a location is specified with .json. If the location does not end with .json, the program will assume it is a directory. Annotations will be stored in this directory with a name that corresponds to the image that the annotation was made on. +- The first time you run labelme, it will create a config file in `~/.labelmerc`. You can edit this file and the changes will be applied the next time that you launch labelme. If you would prefer to use a config file from another location, you can specify this file with the `--config` flag. +- Without the `--nosortlabels` flag, the program will list labels in alphabetical order. When the program is run with this flag, it will display labels in the order that they are provided. +- Flags are assigned to an entire image. [Example](examples/classification) +- Labels are assigned to a single polygon. [Example](examples/bbox_detection) + +### FAQ + +- **How to convert JSON file to numpy array?** See [examples/tutorial](examples/tutorial#convert-to-dataset). +- **How to load label PNG file?** See [examples/tutorial](examples/tutorial#how-to-load-label-png-file). +- **How to get annotations for semantic segmentation?** See [examples/semantic_segmentation](examples/semantic_segmentation). +- **How to get annotations for instance segmentation?** See [examples/instance_segmentation](examples/instance_segmentation). + + +## Examples + +* [Image Classification](examples/classification) +* [Bounding Box Detection](examples/bbox_detection) +* [Semantic Segmentation](examples/semantic_segmentation) +* [Instance Segmentation](examples/instance_segmentation) +* [Video Annotation](examples/video_annotation) + +## How to develop + +```bash +git clone https://github.com/wkentaro/labelme.git +cd labelme + +# Install anaconda3 and labelme +curl -L https://github.com/wkentaro/dotfiles/raw/main/local/bin/install_anaconda3.sh | bash -s . +source .anaconda3/bin/activate +pip install -e . +``` + + +### How to build standalone executable + +Below shows how to build the standalone executable on macOS, Linux and Windows. + +```bash +# Setup conda +conda create --name labelme python=3.9 +conda activate labelme + +# Build the standalone executable +pip install . +pip install 'matplotlib<3.3' +pip install pyinstaller +pyinstaller labelme.spec +dist/labelme --version +``` + + +### How to contribute + +Make sure below test passes on your environment. +See `.github/workflows/ci.yml` for more detail. + +```bash +pip install -r requirements-dev.txt + +ruff format --check # `ruff format` to auto-fix +ruff check # `ruff check --fix` to auto-fix +MPLBACKEND='agg' pytest -vsx tests/ +``` + + +## Acknowledgement + +This repo is the fork of [mpitid/pylabelme](https://github.com/mpitid/pylabelme). diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..171c7f5 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + gui diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..994f110 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +github2pypi==1.0.0 +pytest +pytest-qt +ruff==0.1.9 +twine diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1e3f95b --- /dev/null +++ b/ruff.toml @@ -0,0 +1,33 @@ +exclude = [ + ".conda", + ".git", + "src", +] + +line-length = 88 +indent-width = 4 + +[lint] +# Enable Pyflakes (`F`), pycodestyle (`E`), isort (`I`). +select = ["E", "F", "I"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[isort] +force-single-line = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2566942 --- /dev/null +++ b/setup.py @@ -0,0 +1,164 @@ +import distutils.spawn +import os +import re +import shlex +import subprocess +import sys + +from setuptools import find_packages +from setuptools import setup + + +def get_version(): + filename = "labelme/__init__.py" + with open(filename) as f: + match = re.search(r"""^__version__ = ['"]([^'"]*)['"]""", f.read(), re.M) + if not match: + raise RuntimeError("{} doesn't contain __version__".format(filename)) + version = match.groups()[0] + return version + + +def get_install_requires(): + install_requires = [ + "gdown", + "imgviz>=1.7.5", + "matplotlib", + "natsort>=7.1.0", + "numpy", + "onnxruntime>=1.14.1,!=1.16.0", + "Pillow>=2.8", + "PyYAML", + "qtpy!=1.11.2", + "scikit-image", + "termcolor", + "filterpy", + "opencv-python==4.1.2.30", + "scipy", + "scikit-learn" + ] + + # Find python binding for qt with priority: + # PyQt5 -> PySide2 + # and PyQt5 is automatically installed on Python3. + QT_BINDING = None + + try: + import PyQt5 # NOQA + + QT_BINDING = "pyqt5" + except ImportError: + pass + + if QT_BINDING is None: + try: + import PySide2 # NOQA + + QT_BINDING = "pyside2" + except ImportError: + pass + + if QT_BINDING is None: + # PyQt5 can be installed via pip for Python3 + # 5.15.3, 5.15.4 won't work with PyInstaller + install_requires.append("PyQt5!=5.15.3,!=5.15.4") + QT_BINDING = "pyqt5" + + del QT_BINDING + + if os.name == "nt": # Windows + install_requires.append("colorama") + + return install_requires + + +def get_long_description(): + with open("README.md") as f: + long_description = f.read() + try: + # when this package is being released + import github2pypi + + return github2pypi.replace_url( + slug="wkentaro/labelme", content=long_description, branch="main" + ) + except ImportError: + # when this package is being installed + return long_description + + +def main(): + version = get_version() + + if sys.argv[1] == "release": + try: + import github2pypi # NOQA + except ImportError: + print( + "Please install github2pypi\n\n\tpip install github2pypi\n", + file=sys.stderr, + ) + sys.exit(1) + + if not distutils.spawn.find_executable("twine"): + print( + "Please install twine:\n\n\tpip install twine\n", + file=sys.stderr, + ) + sys.exit(1) + + commands = [ + "git push origin main", + "git tag v{:s}".format(version), + "git push origin --tags", + "python setup.py sdist", + "twine upload dist/labelme-{:s}.tar.gz".format(version), + ] + for cmd in commands: + print("+ {:s}".format(cmd)) + subprocess.check_call(shlex.split(cmd)) + sys.exit(0) + + setup( + name="labelme", + version=version, + packages=find_packages(), + description="Image Polygonal Annotation with Python", + long_description=get_long_description(), + long_description_content_type="text/markdown", + author="Kentaro Wada", + author_email="www.kentaro.wada@gmail.com", + url="https://github.com/wkentaro/labelme", + install_requires=get_install_requires(), + license="GPLv3", + keywords="Image Annotation, Machine Learning", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3 :: Only", + ], + package_data={"labelme": ["icons/*", "config/*.yaml", "translate/*"]}, + entry_points={ + "console_scripts": [ + "labelme=labelme.__main__:main", + "labelme_draw_json=labelme.cli.draw_json:main", + "labelme_draw_label_png=labelme.cli.draw_label_png:main", + "labelme_json_to_dataset=labelme.cli.json_to_dataset:main", + "labelme_export_json=labelme.cli.export_json:main", + "labelme_on_docker=labelme.cli.on_docker:main", + ], + }, + ) + + +if __name__ == "__main__": + main() diff --git a/test_interpolation.py b/test_interpolation.py new file mode 100644 index 0000000..0989869 --- /dev/null +++ b/test_interpolation.py @@ -0,0 +1,164 @@ +import numpy as np +from sklearn.gaussian_process import GaussianProcessRegressor +from sklearn.gaussian_process.kernels import RBF +import os +import glob +from sklearn import preprocessing +import scipy +import sys +import scipy.spatial +from scipy import interpolate +import cv2 + +def cvt_xyxy2xywh(old_bboxes): + new_bboxes = np.zeros(old_bboxes.shape) + new_bboxes[:,0] = (old_bboxes[:,0]+old_bboxes[:,2])/2 + new_bboxes[:,1] = (old_bboxes[:,1]+old_bboxes[:,3])/2 + new_bboxes[:,2] = old_bboxes[:,2] - old_bboxes[:,0] + new_bboxes[:,3] = old_bboxes[:,3] - old_bboxes[:,1] + return new_bboxes + +def cvt_xywh2xyxy(old_bboxes): + new_bboxes = np.zeros(old_bboxes.shape) + dw = old_bboxes[:,2]/2 + dh = old_bboxes[:,3]/2 + new_bboxes[:,0] = old_bboxes[:,0] - dw + new_bboxes[:,1] = old_bboxes[:,1] - dh + new_bboxes[:,2] = old_bboxes[:,0] + dw + new_bboxes[:,3] = old_bboxes[:,1] + dh + return new_bboxes + + +s_e = [130,330] +id = 7 +anno_folder = "/home/tpware/Downloads/dl_asgn1/frame_multiviews/anno2" +txt_paths = sorted(glob.glob(os.path.join(anno_folder,"*.txt")),key=lambda x:int(x.split('/')[-1].split('.txt')[0])) + +img_folder = "/home/tpware/Downloads/dl_asgn1/frame_multiviews/moving_view2" +img_paths = sorted(glob.glob(os.path.join(img_folder,"*.jpg")),key=lambda x:int(x.split('/')[-1].split('.jpg')[0])) + +save_folder = "/home/tpware/projects/Tracking_Tool/data/interpolated_data/" + +xyxy_bboxes = [] +for jdx in range(len(txt_paths)): + txt_p = txt_paths[jdx] + if int(os.path.basename(txt_p).split('.')[0]) >= s_e[0] and int(os.path.basename(txt_p).split('.')[0]) <= s_e[1]: + with open(txt_p) as f: + lines = [line.rstrip('\n') for line in f] + for kdx in range(len(lines)): + if ' 7 ' in lines[kdx]: + xyxy_bboxes.append(lines[kdx].split(' ')[2:]) + +xyxy_bboxes = np.array(xyxy_bboxes).astype(int) +xywh_bboxes = cvt_xyxy2xywh(xyxy_bboxes) + +img_indices = np.linspace(0,xywh_bboxes.shape[0]-1,num=10,dtype=int) + + + +from sklearn.gaussian_process import GaussianProcessRegressor +from sklearn.gaussian_process.kernels import RationalQuadratic + + +# interpolated_data = [] +# for jdx in range(4): +# kernel = RationalQuadratic() +# gpr = GaussianProcessRegressor(kernel=kernel,random_state=0).fit(img_indices.reshape(-1,1), xywh_bboxes[:,jdx][img_indices]) +# print('Score:',gpr.score(img_indices.reshape(-1,1), xywh_bboxes[:,jdx][img_indices])) +# interpolated_data.append(gpr.predict(np.arange(0,xywh_bboxes.shape[0]).reshape(-1,1), return_std=False)) + +# # import ipdb;ipdb.set_trace() +# interpolated_data = np.stack(interpolated_data,axis=1) +# cvt_interpolated_data = cvt_xywh2xyxy(interpolated_data).astype(int) + +# cnt = 0 +# for jdx in range(len(txt_paths)): +# img_p = img_paths[jdx] +# if int(os.path.basename(img_p).split('.')[0]) >= s_e[0] and int(os.path.basename(img_p).split('.')[0]) <= s_e[1]: +# img = cv2.imread(img_p) +# box = cvt_interpolated_data[cnt] +# cv2.rectangle(img,(box[0],box[1]),(box[2],box[3]),(255,0,0),2) +# cv2.imwrite(save_folder+os.path.basename(img_p),img) +# cnt+=1 + + +# dont work +""" +interpolated_data = [[],[],[],[]] +fdx_start = 0 +for fdx in range(1,len(img_indices)): + for jdx in range(4): + kernel = RationalQuadratic() + # import ipdb; ipdb.set_trace() + gpr = GaussianProcessRegressor(kernel=kernel,random_state=0).fit(np.array([0,int(img_indices[2]-img_indices[1])]).reshape(-1,1), xywh_bboxes[:,jdx][img_indices[fdx-1:fdx+1]]) + interpolated_data[jdx].extend(gpr.predict(np.arange(0,img_indices[fdx]-img_indices[fdx-1]).reshape(-1,1), return_std=False)) + fdx_start += 1 + + + +interpolated_data = np.stack(interpolated_data,axis=1) +cvt_interpolated_data = cvt_xywh2xyxy(interpolated_data).astype(int) + +cnt = 0 +for jdx in range(len(txt_paths)): + img_p = img_paths[jdx] + if int(os.path.basename(img_p).split('.')[0]) >= s_e[0] and int(os.path.basename(img_p).split('.')[0]) <= s_e[1]: + img = cv2.imread(img_p) + box = cvt_interpolated_data[cnt] + cv2.rectangle(img,(box[0],box[1]),(box[2],box[3]),(0,255,0),2) + cv2.imwrite(save_folder+os.path.basename(img_p),img) + cnt+=1 +""" + + + +# interpolated_data = [] +# for jdx in range(4): +# f = interpolate.interp1d(img_indices, xywh_bboxes[:,jdx][img_indices]) +# filled_data = f(np.arange(0,xywh_bboxes.shape[0])) +# interpolated_data.append(filled_data) + +# interpolated_data = np.stack(interpolated_data,axis=1) +# cvt_interpolated_data = cvt_xywh2xyxy(interpolated_data).astype(int) + +# cnt = 0 +# for jdx in range(len(txt_paths)): +# img_p = img_paths[jdx] +# if int(os.path.basename(img_p).split('.')[0]) >= s_e[0] and int(os.path.basename(img_p).split('.')[0]) <= s_e[1]: +# img = cv2.imread(img_p) +# box = cvt_interpolated_data[cnt] +# cv2.rectangle(img,(box[0],box[1]),(box[2],box[3]),(255,0,0),2) +# cv2.imwrite(save_folder+os.path.basename(img_p),img) +# cnt+=1 + + + + + + + +# import ipdb; ipdb.set_trace() +# img_indices = np.linspace(0,xyxy_bboxes.shape[0]-1,num=10,dtype=int) + +# interpolated_data = [] +# for jdx in range(4): +# f = interpolate.interp1d(img_indices, xyxy_bboxes[:,jdx][img_indices]) +# filled_data = f(np.arange(0,xyxy_bboxes.shape[0])) +# interpolated_data.append(filled_data) + +# interpolated_data = np.stack(interpolated_data,axis=1).astype(int) + +# cnt = 0 +# for jdx in range(len(txt_paths)): +# img_p = img_paths[jdx] +# if int(os.path.basename(img_p).split('.')[0]) >= s_e[0] and int(os.path.basename(img_p).split('.')[0]) <= s_e[1]: +# img = cv2.imread(img_p) +# box = interpolated_data[cnt] +# cv2.rectangle(img,(box[0],box[1]),(box[2],box[3]),(255,0,0),2) +# cv2.imwrite(save_folder+os.path.basename(img_p),img) +# cnt+=1 + + +# import ipdb; ipdb.set_trace() + + diff --git a/tests/labelme_tests/__init__.py b/tests/labelme_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/labelme_tests/data/annotated/2011_000003.jpg b/tests/labelme_tests/data/annotated/2011_000003.jpg new file mode 120000 index 0000000..43d4c70 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000003.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000003.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated/2011_000003.json b/tests/labelme_tests/data/annotated/2011_000003.json new file mode 120000 index 0000000..2ad4db7 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000003.json @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000003.json \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated/2011_000006.jpg b/tests/labelme_tests/data/annotated/2011_000006.jpg new file mode 120000 index 0000000..b676466 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000006.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000006.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated/2011_000006.json b/tests/labelme_tests/data/annotated/2011_000006.json new file mode 120000 index 0000000..d049155 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000006.json @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000006.json \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated/2011_000025.jpg b/tests/labelme_tests/data/annotated/2011_000025.jpg new file mode 120000 index 0000000..bd14ce1 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000025.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000025.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated/2011_000025.json b/tests/labelme_tests/data/annotated/2011_000025.json new file mode 120000 index 0000000..6545de4 --- /dev/null +++ b/tests/labelme_tests/data/annotated/2011_000025.json @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000025.json \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.jpg b/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.jpg new file mode 120000 index 0000000..6e143eb --- /dev/null +++ b/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.jpg @@ -0,0 +1 @@ +../../../../examples/tutorial/apc2016_obj3.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.json b/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.json new file mode 120000 index 0000000..0df49fa --- /dev/null +++ b/tests/labelme_tests/data/annotated_with_data/apc2016_obj3.json @@ -0,0 +1 @@ +../../../../examples/tutorial/apc2016_obj3.json \ No newline at end of file diff --git a/tests/labelme_tests/data/raw/2011_000003.jpg b/tests/labelme_tests/data/raw/2011_000003.jpg new file mode 120000 index 0000000..43d4c70 --- /dev/null +++ b/tests/labelme_tests/data/raw/2011_000003.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000003.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/raw/2011_000006.jpg b/tests/labelme_tests/data/raw/2011_000006.jpg new file mode 120000 index 0000000..b676466 --- /dev/null +++ b/tests/labelme_tests/data/raw/2011_000006.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000006.jpg \ No newline at end of file diff --git a/tests/labelme_tests/data/raw/2011_000025.jpg b/tests/labelme_tests/data/raw/2011_000025.jpg new file mode 120000 index 0000000..bd14ce1 --- /dev/null +++ b/tests/labelme_tests/data/raw/2011_000025.jpg @@ -0,0 +1 @@ +../../../../examples/instance_segmentation/data_annotated/2011_000025.jpg \ No newline at end of file diff --git a/tests/labelme_tests/test_app.py b/tests/labelme_tests/test_app.py new file mode 100644 index 0000000..caffe95 --- /dev/null +++ b/tests/labelme_tests/test_app.py @@ -0,0 +1,114 @@ +import os.path as osp +import shutil +import tempfile + +import pytest + +import labelme.app +import labelme.config +import labelme.testing + +here = osp.dirname(osp.abspath(__file__)) +data_dir = osp.join(here, "data") + + +def _win_show_and_wait_imageData(qtbot, win): + win.show() + + def check_imageData(): + assert hasattr(win, "imageData") + assert win.imageData is not None + + qtbot.waitUntil(check_imageData) # wait for loadFile + + +@pytest.mark.gui +def test_MainWindow_open(qtbot): + win = labelme.app.MainWindow() + qtbot.addWidget(win) + win.show() + win.close() + + +@pytest.mark.gui +def test_MainWindow_open_img(qtbot): + img_file = osp.join(data_dir, "raw/2011_000003.jpg") + win = labelme.app.MainWindow(filename=img_file) + qtbot.addWidget(win) + _win_show_and_wait_imageData(qtbot, win) + win.close() + + +@pytest.mark.gui +def test_MainWindow_open_json(qtbot): + json_files = [ + osp.join(data_dir, "annotated_with_data/apc2016_obj3.json"), + osp.join(data_dir, "annotated/2011_000003.json"), + ] + for json_file in json_files: + labelme.testing.assert_labelfile_sanity(json_file) + + win = labelme.app.MainWindow(filename=json_file) + qtbot.addWidget(win) + _win_show_and_wait_imageData(qtbot, win) + win.close() + + +def create_MainWindow_with_directory(qtbot): + directory = osp.join(data_dir, "raw") + win = labelme.app.MainWindow(filename=directory) + qtbot.addWidget(win) + _win_show_and_wait_imageData(qtbot, win) + return win + + +@pytest.mark.gui +def test_MainWindow_openNextImg(qtbot): + win = create_MainWindow_with_directory(qtbot) + win.openNextImg() + + +@pytest.mark.gui +def test_MainWindow_openPrevImg(qtbot): + win = create_MainWindow_with_directory(qtbot) + win.openNextImg() + + +@pytest.mark.gui +def test_MainWindow_annotate_jpg(qtbot): + tmp_dir = tempfile.mkdtemp() + input_file = osp.join(data_dir, "raw/2011_000003.jpg") + out_file = osp.join(tmp_dir, "2011_000003.json") + + config = labelme.config.get_default_config() + win = labelme.app.MainWindow( + config=config, + filename=input_file, + output_file=out_file, + ) + qtbot.addWidget(win) + _win_show_and_wait_imageData(qtbot, win) + + label = "whole" + points = [ + (100, 100), + (100, 238), + (400, 238), + (400, 100), + ] + shapes = [ + dict( + label=label, + group_id=None, + points=points, + shape_type="polygon", + mask=None, + flags={}, + other_data={}, + ) + ] + win.loadLabels(shapes) + win.saveFile() + + labelme.testing.assert_labelfile_sanity(out_file) + shutil.rmtree(tmp_dir) diff --git a/tests/labelme_tests/utils_tests/__init__.py b/tests/labelme_tests/utils_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/labelme_tests/utils_tests/test_image.py b/tests/labelme_tests/utils_tests/test_image.py new file mode 100644 index 0000000..d9abc93 --- /dev/null +++ b/tests/labelme_tests/utils_tests/test_image.py @@ -0,0 +1,31 @@ +import os.path as osp + +import numpy as np +import PIL.Image + +from labelme.utils import image as image_module + +from .util import data_dir +from .util import get_img_and_data + + +def test_img_b64_to_arr(): + img, _ = get_img_and_data() + assert img.dtype == np.uint8 + assert img.shape == (907, 1210, 3) + + +def test_img_arr_to_b64(): + img_file = osp.join(data_dir, "annotated_with_data/apc2016_obj3.jpg") + img_arr = np.asarray(PIL.Image.open(img_file)) + img_b64 = image_module.img_arr_to_b64(img_arr) + img_arr2 = image_module.img_b64_to_arr(img_b64) + np.testing.assert_allclose(img_arr, img_arr2) + + +def test_img_data_to_png_data(): + img_file = osp.join(data_dir, "annotated_with_data/apc2016_obj3.jpg") + with open(img_file, "rb") as f: + img_data = f.read() + png_data = image_module.img_data_to_png_data(img_data) + assert isinstance(png_data, bytes) diff --git a/tests/labelme_tests/utils_tests/test_shape.py b/tests/labelme_tests/utils_tests/test_shape.py new file mode 100644 index 0000000..98ed8ed --- /dev/null +++ b/tests/labelme_tests/utils_tests/test_shape.py @@ -0,0 +1,24 @@ +from labelme.utils import shape as shape_module + +from .util import get_img_and_data + + +def test_shapes_to_label(): + img, data = get_img_and_data() + label_name_to_value = {} + for shape in data["shapes"]: + label_name = shape["label"] + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + cls, _ = shape_module.shapes_to_label( + img.shape, data["shapes"], label_name_to_value + ) + assert cls.shape == img.shape[:2] + + +def test_shape_to_mask(): + img, data = get_img_and_data() + for shape in data["shapes"]: + points = shape["points"] + mask = shape_module.shape_to_mask(img.shape[:2], points) + assert mask.shape == img.shape[:2] diff --git a/tests/labelme_tests/utils_tests/util.py b/tests/labelme_tests/utils_tests/util.py new file mode 100644 index 0000000..008645b --- /dev/null +++ b/tests/labelme_tests/utils_tests/util.py @@ -0,0 +1,37 @@ +import json +import os.path as osp + +from labelme.utils import image as image_module +from labelme.utils import shape as shape_module + +here = osp.dirname(osp.abspath(__file__)) +data_dir = osp.join(here, "../data") + + +def get_img_and_data(): + json_file = osp.join(data_dir, "annotated_with_data/apc2016_obj3.json") + with open(json_file) as f: + data = json.load(f) + img_b64 = data["imageData"] + img = image_module.img_b64_to_arr(img_b64) + return img, data + + +def get_img_and_lbl(): + img, data = get_img_and_data() + + label_name_to_value = {"__background__": 0} + for shape in data["shapes"]: + label_name = shape["label"] + label_value = len(label_name_to_value) + label_name_to_value[label_name] = label_value + + n_labels = max(label_name_to_value.values()) + 1 + label_names = [None] * n_labels + for label_name, label_value in label_name_to_value.items(): + label_names[label_value] = label_name + + lbl, _ = shape_module.shapes_to_label( + img.shape, data["shapes"], label_name_to_value + ) + return img, lbl, label_names diff --git a/tests/labelme_tests/widgets_tests/__init__.py b/tests/labelme_tests/widgets_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/labelme_tests/widgets_tests/test_label_dialog.py b/tests/labelme_tests/widgets_tests/test_label_dialog.py new file mode 100644 index 0000000..aef525b --- /dev/null +++ b/tests/labelme_tests/widgets_tests/test_label_dialog.py @@ -0,0 +1,93 @@ +import pytest +from qtpy import QtCore +from qtpy import QtWidgets + +from labelme.widgets import LabelDialog +from labelme.widgets import LabelQLineEdit + + +@pytest.mark.gui +def test_LabelQLineEdit(qtbot): + list_widget = QtWidgets.QListWidget() + list_widget.addItems(["cat", "dog", "person"]) + widget = LabelQLineEdit() + widget.setListWidget(list_widget) + qtbot.addWidget(widget) + + # key press to navigate in label list + item = widget.list_widget.findItems("cat", QtCore.Qt.MatchExactly)[0] + widget.list_widget.setCurrentItem(item) + assert widget.list_widget.currentItem().text() == "cat" + qtbot.keyPress(widget, QtCore.Qt.Key_Down) + assert widget.list_widget.currentItem().text() == "dog" + + # key press to enter label + qtbot.keyPress(widget, QtCore.Qt.Key_P) + qtbot.keyPress(widget, QtCore.Qt.Key_E) + qtbot.keyPress(widget, QtCore.Qt.Key_R) + qtbot.keyPress(widget, QtCore.Qt.Key_S) + qtbot.keyPress(widget, QtCore.Qt.Key_O) + qtbot.keyPress(widget, QtCore.Qt.Key_N) + assert widget.text() == "person" + + +@pytest.mark.gui +def test_LabelDialog_addLabelHistory(qtbot): + labels = ["cat", "dog", "person"] + widget = LabelDialog(labels=labels, sort_labels=True) + qtbot.addWidget(widget) + + widget.addLabelHistory("bicycle") + assert widget.labelList.count() == 4 + widget.addLabelHistory("bicycle") + assert widget.labelList.count() == 4 + item = widget.labelList.item(0) + assert item.text() == "bicycle" + + +@pytest.mark.gui +def test_LabelDialog_popUp(qtbot): + labels = ["cat", "dog", "person"] + widget = LabelDialog(labels=labels, sort_labels=True) + qtbot.addWidget(widget) + + # popUp(text='cat') + + def interact(): + qtbot.keyClick(widget.edit, QtCore.Qt.Key_P) # enter 'p' for 'person' # NOQA + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + + QtCore.QTimer.singleShot(500, interact) + label, flags, group_id, description = widget.popUp("cat") + assert label == "person" + assert flags == {} + assert group_id is None + assert description == "" + + # popUp() + + def interact(): + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + + QtCore.QTimer.singleShot(500, interact) + label, flags, group_id, description = widget.popUp() + assert label == "person" + assert flags == {} + assert group_id is None + assert description == "" + + # popUp() + key_Up + + def interact(): + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Up) # 'person' -> 'dog' # NOQA + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + qtbot.keyClick(widget.edit, QtCore.Qt.Key_Enter) # NOQA + + QtCore.QTimer.singleShot(500, interact) + label, flags, group_id, description = widget.popUp() + assert label == "dog" + assert flags == {} + assert group_id is None + assert description == "" diff --git a/tests/labelme_tests/widgets_tests/test_label_list_widget.py b/tests/labelme_tests/widgets_tests/test_label_list_widget.py new file mode 100644 index 0000000..fb840e5 --- /dev/null +++ b/tests/labelme_tests/widgets_tests/test_label_list_widget.py @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- + +import pytest + +from labelme.widgets import LabelListWidget +from labelme.widgets import LabelListWidgetItem + + +@pytest.mark.gui +def test_LabelListWidget(qtbot): + widget = LabelListWidget() + + item = LabelListWidgetItem(text="person ●") + widget.addItem(item) + item = LabelListWidgetItem(text="dog ●") + widget.addItem(item) + + widget.show() + qtbot.addWidget(widget) + qtbot.waitExposed(widget)