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
+
+
+
+
+
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
+
+
+
+
+
+
+
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
+
+
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
\ 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 @@
+
+
+
\ 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 @@
+
+
+
+
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 @@
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MainWindow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+ 图像
+
+
+
+
+ 点击并拖拽以移动控制点
+
+
+
+
+ 点击并拖拽以移动形状'%s'
+
+
+
+ MainWindow
+
+
+
+ 标记
+
+
+
+
+ 多边形标签
+
+
+
+
+ 选择标签类型并开始以其标注。按'Esc'取消选择。
+
+
+
+
+ 标签列表
+
+
+
+
+ 按文件名检索
+
+
+
+
+ 文件列表
+
+
+
+
+ 退出(&Q)
+
+
+
+
+ 退出应用
+
+
+
+
+ 打开(&O)
+
+
+
+
+ 打开图像或标签文件
+
+
+
+
+ 打开目录(&O)
+
+
+
+
+ 打开目录
+
+
+
+
+ 下一幅(&N)
+
+
+
+
+ 打开下一幅 (按Ctl+Shift拷贝标签)
+
+
+
+
+ 上一幅(&P)
+
+
+
+
+ 打开上一幅 (按Ctl+Shift拷贝标签)
+
+
+
+
+ 保存(&S)
+
+
+
+
+ 保存标签到文件
+
+
+
+
+ 另存为(&S)
+
+
+
+
+ 保存标签到不同的文件
+
+
+
+
+ 删除(&D)
+
+
+
+
+ 删除当前标签文件
+
+
+
+
+ 更改输出路径(&C)
+
+
+
+
+ 更改载入、保存标注的路径
+
+
+
+
+ 自动保存(&A)
+
+
+
+
+ 自动保存
+
+
+
+
+ 关闭(&C)
+
+
+
+
+ 关闭当前文件
+
+
+
+
+ 多边形描边颜色(&L)
+
+
+
+
+ 选择多边形描边颜色
+
+
+
+
+ 多边形填充颜色(&F)
+
+
+
+
+ 选择多边形填充颜色
+
+
+
+
+ 保留最后的标注
+
+
+
+
+ 开关“保留最后的标注”模式
+
+
+
+
+ 创建多边形
+
+
+
+
+ 开始绘制多边形
+
+
+
+
+ 创建矩形
+
+
+
+
+ 开始绘制矩形
+
+
+
+
+ 创建圆形
+
+
+
+
+ 开始绘制圆形
+
+
+
+
+ 创建直线
+
+
+
+
+ 开始创建直线
+
+
+
+
+ 创建控制点
+
+
+
+
+ 开始绘制控制点
+
+
+
+
+ 创建折线
+
+
+
+
+ 开始绘制折线。Ctrl+单击左键结束绘制。
+
+
+
+
+ 编辑多边形
+
+
+
+
+ 移动、编辑选中的多边形
+
+
+
+
+ 删除多边形
+
+
+
+
+ 删除选中的多边形
+
+
+
+
+ 复制多边形
+
+
+
+
+ 为选中的多边形创建副本
+
+
+
+
+ 撤销最后的控制点
+
+
+
+
+ 撤销最后一次绘制的控制点
+
+
+
+
+ 在边上加入控制点
+
+
+
+
+ 在最近的边上加一个控制点
+
+
+
+
+ 撤销
+
+
+
+
+ 撤销最近一次添加和编辑
+
+
+
+
+ 隐藏多边形(&H)
+
+
+
+
+ 隐藏多边形(&H)
+
+
+
+
+ 显示多边形(&S)
+
+
+
+
+ 显示所有多边形
+
+
+
+
+ 开关多边形(&S)
+
+
+
+
+ 开关所有多边形
+
+
+
+
+ 教程[&T]
+
+
+
+
+ 显示教程网页
+
+
+
+
+ 缩放图像。亦可从画布的{}和{}访问
+
+
+
+
+ Ctrl+滚轮
+
+
+
+
+ 放大(&I)
+
+
+
+
+ 增加缩放水平
+
+
+
+
+ 缩小(&Z)
+
+
+
+
+ 减小缩放水平
+
+
+
+
+ 原始大小(&O)
+
+
+
+
+ 缩放至原始大小
+
+
+
+
+ 适应窗口(&F)
+
+
+
+
+ 跟随窗口大小缩放
+
+
+
+
+ 适应宽度(&W)
+
+
+
+
+ 跟随窗口宽度缩放
+
+
+
+
+ 编辑标签(&E)
+
+
+
+
+ 修改选中多边形的标签
+
+
+
+
+ 形状描边颜色(&L)
+
+
+
+
+ 为此多边形修改描边颜色
+
+
+
+
+ 形状填充颜色(&F)
+
+
+
+
+ 为此多边形修改填充颜色
+
+
+
+
+ 填充所绘多边形
+
+
+
+
+ 绘制时填充多边形
+
+
+
+
+ 文件(&F)
+
+
+
+
+ 编辑(&E)
+
+
+
+
+ 视图(&V)
+
+
+
+
+ 帮助(&H)
+
+
+
+
+ 最近打开(&R)
+
+
+
+
+ %s 启动完了
+
+
+
+
+ 无效的标签
+
+
+
+
+ 无效的标签'{}',验证类型'{}'
+
+
+
+
+ 保存标签发生错误
+
+
+
+
+ <b>%s</b>
+
+
+
+
+ 打开文件发生错误
+
+
+
+
+ 文件不存在: <b>%s</b>
+
+
+
+
+ 正在载入 %s...
+
+
+
+
+ <p><b>%s</b></p><p>请确认<i>%s</i>是一个合法的标签文件。
+
+
+
+
+ 打开文件发生错误 %s
+
+
+
+
+ lt;p>请确认<i>{0}</i>是一个合法的图像文件。<br/>支持的格式包括: {1}</p>
+
+
+
+
+ 已加载 %s
+
+
+
+
+ 图像和标签文件(%s)
+
+
+
+
+ %s - 选择图像或标签文件
+
+
+
+
+ %s - 保存和加载批注的路径
+
+
+
+
+ %s . 批注会被加载和保存在 %s
+
+
+
+
+ %s - 选择文件
+
+
+
+
+ 标签文件(*%s)
+
+
+
+
+ 选择文件
+
+
+
+
+ 即将永久性删除此标签文件。还要继续吗?
+
+
+
+
+ 注意
+
+
+
+
+ 关闭前保存批注到"{}"吗?
+
+
+
+
+ 保存批注吗?
+
+
+
+
+ 选择描边颜色
+
+
+
+
+ 选择填充颜色
+
+
+
+
+ 即将永久性删除多边形{}。还要继续吗?
+
+
+
+
+ %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)