├── .cursorrules ├── .github ├── FUNDING.yml └── workflows │ ├── python-publish-cpu.yml │ ├── python-publish-gpu.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── launch.json └── settings.json ├── CITATION.cff ├── LICENSE ├── MANIFEST.in ├── README.md ├── anylabeling.desktop ├── anylabeling.spec ├── anylabeling ├── README.md ├── __init__.py ├── app.py ├── app_info.py ├── config.py ├── configs │ ├── __init__.py │ ├── anylabeling_config.yaml │ └── auto_labeling │ │ ├── __init__.py │ │ └── models.yaml ├── resources │ ├── __init__.py │ ├── icons │ │ ├── export.svg │ │ └── tools.svg │ ├── images │ │ ├── box.png │ │ ├── brain.png │ │ ├── cancel.png │ │ ├── cartesian.png │ │ ├── check.png │ │ ├── circle.png │ │ ├── cn.png │ │ ├── color.png │ │ ├── computer.png │ │ ├── copy.png │ │ ├── delete.png │ │ ├── done.png │ │ ├── edit.png │ │ ├── expert.png │ │ ├── expert1.png │ │ ├── expert2.png │ │ ├── eye.png │ │ ├── feBlend-icon.png │ │ ├── file.png │ │ ├── fit-width.png │ │ ├── fit-window.png │ │ ├── fit.png │ │ ├── format_createml.png │ │ ├── format_voc.png │ │ ├── format_yolo.png │ │ ├── group.png │ │ ├── help.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ ├── labels.png │ │ ├── line-strip.png │ │ ├── line.png │ │ ├── logo.png │ │ ├── minus.png │ │ ├── moon.png │ │ ├── new.png │ │ ├── next.png │ │ ├── objects.png │ │ ├── open.png │ │ ├── paste.png │ │ ├── plus.png │ │ ├── point.png │ │ ├── polygon.png │ │ ├── prev.png │ │ ├── quit.png │ │ ├── rectangle.png │ │ ├── resetall.png │ │ ├── save-as.png │ │ ├── save.png │ │ ├── scissors.png │ │ ├── sun.png │ │ ├── undo-cross.png │ │ ├── undo.png │ │ ├── upload_brain.png │ │ ├── us.png │ │ ├── verify.png │ │ ├── vn.png │ │ ├── zoom-in.png │ │ ├── zoom-out.png │ │ └── zoom.png │ ├── resources.py │ ├── resources.qrc │ └── translations │ │ ├── en_US.qm │ │ ├── en_US.ts │ │ ├── vi_VN.qm │ │ ├── vi_VN.ts │ │ ├── zh_CN.qm │ │ └── zh_CN.ts ├── services │ ├── __init__.py │ └── auto_labeling │ │ ├── __init__.py │ │ ├── lru_cache.py │ │ ├── model.py │ │ ├── model_manager.py │ │ ├── sam2_onnx.py │ │ ├── sam_onnx.py │ │ ├── segment_anything.py │ │ ├── types.py │ │ ├── yolov5.py │ │ └── yolov8.py ├── styles │ ├── __init__.py │ └── theme.py ├── utils.py └── views │ ├── __init__.py │ ├── common │ ├── __init__.py │ └── toaster.py │ ├── labeling │ ├── __init__.py │ ├── label_file.py │ ├── label_widget.py │ ├── label_wrapper.py │ ├── logger.py │ ├── shape.py │ ├── testing.py │ ├── utils │ │ ├── __init__.py │ │ ├── _io.py │ │ ├── export_formats.py │ │ ├── export_worker.py │ │ ├── image.py │ │ ├── opencv.py │ │ ├── qt.py │ │ └── shape.py │ └── widgets │ │ ├── __init__.py │ │ ├── auto_labeling │ │ ├── __init__.py │ │ ├── auto_labeling.py │ │ └── auto_labeling.ui │ │ ├── brightness_contrast_dialog.py │ │ ├── canvas.py │ │ ├── color_dialog.py │ │ ├── escapable_qlist_widget.py │ │ ├── export_dialog.py │ │ ├── file_dialog_preview.py │ │ ├── label_dialog.py │ │ ├── label_list_widget.py │ │ ├── toolbar.py │ │ ├── unique_label_qlist_widget.py │ │ └── zoom_widget.py │ └── mainwindow.py ├── assets └── screenshot.png ├── docs └── macos_folder_mode.md ├── pyproject.toml ├── requirements-dev.txt ├── requirements-gpu-dev.txt ├── requirements-gpu.txt ├── requirements-macos-dev.txt ├── requirements-macos.txt ├── requirements.txt ├── sample_images ├── erol-ahmed-leOh1CzRZVQ-unsplash.jpg ├── evan-foley-ZgUtMaOVUAY-unsplash.jpg ├── jonas-kakaroto-5JQH9Iqnm9o-unsplash.jpg ├── julien-goettelmann-nMRE6uR-eP4-unsplash.jpg ├── national-cancer-institute-L7en7Lb-Ovc-unsplash.jpg └── ryoji-iwata-n31JPLu8_Pw-unsplash.jpg ├── scripts ├── build_and_publish_pypi.sh ├── build_executable.sh ├── build_macos_folder.sh ├── compile_languages.py ├── generate_languages.py └── zip_models.py └── setup.py /.cursorrules: -------------------------------------------------------------------------------- 1 | - Use PyQt5 for GUI. 2 | - Split code into files when possible. 3 | - Make code clean and understandable. 4 | - Optimize code for performance and memory usage. 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vietanhdev 2 | ko_fi: vietanhdev 3 | -------------------------------------------------------------------------------- /.github/workflows/python-publish-cpu.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish CPU 🐍📦 to PyPI 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build-n-publish: 9 | if: startsWith(github.ref, 'refs/tags/') 10 | name: Build and publish CPU 🐍📦 to PyPI 11 | runs-on: ubuntu-latest 12 | environment: 13 | name: pypi-cpu 14 | url: https://pypi.org/p/anylabeling 15 | permissions: 16 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.x" 23 | - name: Install pypa/build 24 | run: >- 25 | python -m pip install build==1.2.2 twine==6.1.0 --user 26 | - name: Set preferred device to CPU 27 | run: >- 28 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "CPU"/g' anylabeling/app_info.py 29 | - name: Build a binary wheel and a source tarball 30 | run: >- 31 | python -m build --sdist --wheel --outdir dist/ . 32 | - name: Publish distribution 📦 to PyPI 33 | if: startsWith(github.ref, 'refs/tags') 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | skip-existing: true 37 | -------------------------------------------------------------------------------- /.github/workflows/python-publish-gpu.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish GPU 🐍📦 to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | 10 | build-n-publish-gpu: 11 | if: startsWith(github.ref, 'refs/tags/') 12 | name: Build and publish GPU 🐍📦 to PyPI 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: pypi-gpu 16 | url: https://pypi.org/p/anylabeling-gpu 17 | permissions: 18 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: "3.x" 25 | - name: Install pypa/build 26 | run: >- 27 | python -m pip install build==1.2.2 twine==6.1.0 --user 28 | - name: Set preferred device to GPU 29 | run: >- 30 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "GPU"/g' anylabeling/app_info.py 31 | 32 | - name: Build a binary wheel and a source tarball 33 | run: >- 34 | python -m build --wheel --outdir dist/ . 35 | - name: Publish distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | skip-existing: true 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: New Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | if: startsWith(github.ref, 'refs/tags/') 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Build Changelog 18 | id: github_release 19 | uses: mikepenz/release-changelog-builder-action@v3 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | configurationJson: | 24 | { 25 | "template": "## What's Changed\n\n
\nUncategorized\n\n#{{UNCATEGORIZED}}\n
\n\nIf you find this project useful, please consider [sponsoring](https://ko-fi.com/vietanhdev) its development.", 26 | "categories": [ 27 | { 28 | "title": "## 🚀 Features", 29 | "labels": ["feature"] 30 | }, 31 | { 32 | "title": "## 🐛 Fixes", 33 | "labels": ["fix"] 34 | }, 35 | { 36 | "title": "## 💬 Other", 37 | "labels": ["other"] 38 | } 39 | ] 40 | } 41 | 42 | - name: Create Release 43 | id: create_release 44 | uses: softprops/action-gh-release@v2 45 | with: 46 | body: ${{steps.github_release.outputs.changelog}} 47 | draft: true 48 | prerelease: true 49 | tag_name: ${{ github.ref_name }} 50 | make_latest: 'false' 51 | fail_on_unmatched_files: false 52 | 53 | build: 54 | needs: [release] 55 | strategy: 56 | matrix: 57 | os: [ubuntu-latest, windows-latest] 58 | device: [CPU, GPU] 59 | 60 | runs-on: ${{ matrix.os }} 61 | permissions: 62 | contents: write 63 | 64 | steps: 65 | - uses: actions/checkout@v2 66 | with: 67 | submodules: true 68 | 69 | - uses: conda-incubator/setup-miniconda@v2 70 | with: 71 | python-version: "3.10.14" 72 | miniconda-version: "latest" 73 | 74 | - name: Set preferred device 75 | shell: bash -l {0} 76 | run: >- 77 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "${{ matrix.device }}"/g' anylabeling/app_info.py 78 | 79 | - name: Install main 80 | shell: bash -l {0} 81 | run: | 82 | pip install . 83 | 84 | - name: Run pyinstaller 85 | shell: bash -l {0} 86 | run: | 87 | pip install pyinstaller 88 | pyinstaller anylabeling.spec 89 | 90 | - name: Rename executables with better naming convention 91 | shell: bash -l {0} 92 | run: | 93 | if [ "${{ runner.os }}" == "Linux" ]; then 94 | if [ -f "./dist/anylabeling" ]; then 95 | mv ./dist/anylabeling ./dist/AnyLabeling-Linux-${{ matrix.device }}-x64 96 | fi 97 | elif [ "${{ runner.os }}" == "Windows" ]; then 98 | if [ -f "./dist/anylabeling.exe" ]; then 99 | mv ./dist/anylabeling.exe ./dist/AnyLabeling-Windows-${{ matrix.device }}-x64.exe 100 | fi 101 | fi 102 | 103 | - name: Upload Linux/Windows Release Assets 104 | uses: softprops/action-gh-release@v2 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | with: 108 | tag_name: ${{ github.ref_name }} 109 | files: | 110 | ./dist/AnyLabeling-* 111 | fail_on_unmatched_files: false 112 | append_body: true 113 | preserve_order: true 114 | if: success() 115 | 116 | build_macos_folder: 117 | needs: [release] 118 | runs-on: macos-latest 119 | permissions: 120 | contents: write 121 | strategy: 122 | matrix: 123 | device: [CPU, GPU] 124 | 125 | steps: 126 | - uses: actions/checkout@v2 127 | with: 128 | submodules: true 129 | 130 | - uses: conda-incubator/setup-miniconda@v2 131 | with: 132 | python-version: "3.10.14" 133 | miniconda-version: "latest" 134 | 135 | - name: Install PyQt5 for macOS 136 | shell: bash -l {0} 137 | run: | 138 | conda install -c conda-forge pyqt==5.15.7 139 | 140 | - name: Install main 141 | shell: bash -l {0} 142 | run: | 143 | pip install -e . 144 | 145 | - name: Make build script executable 146 | shell: bash -l {0} 147 | run: | 148 | chmod +x scripts/build_macos_folder.sh 149 | 150 | - name: Build in folder mode 151 | shell: bash -l {0} 152 | run: | 153 | ./scripts/build_macos_folder.sh ${{ matrix.device }} 154 | 155 | - name: Rename macOS folder with better naming convention 156 | shell: bash -l {0} 157 | run: | 158 | mv dist/AnyLabeling-Folder${{ matrix.device == 'GPU' && '-GPU' || '' }} dist/AnyLabeling-macOS-${{ matrix.device }} 159 | 160 | - name: Create zip archive 161 | shell: bash -l {0} 162 | run: | 163 | cd dist && zip -r AnyLabeling-macOS-${{ matrix.device }}.zip AnyLabeling-macOS-${{ matrix.device }}/ 164 | 165 | - name: Upload macOS Folder Build Assets 166 | uses: softprops/action-gh-release@v2 167 | env: 168 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 169 | with: 170 | tag_name: ${{ github.ref_name }} 171 | files: ./dist/AnyLabeling-macOS-${{ matrix.device }}.zip 172 | append_body: true 173 | preserve_order: true 174 | if: success() 175 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | *.pyc 4 | *.egg-info 5 | anylabeling/data 6 | /data 7 | /dist 8 | /build 9 | /wheels_dist 10 | *_ui.py 11 | anylabeling/app_info.py-* 12 | zipped_models/* 13 | venv/ 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit run --all-files 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: check-added-large-files 7 | args: ['--maxkb=8096'] 8 | - id: check-case-conflict 9 | - id: check-executables-have-shebangs 10 | - id: check-merge-conflict 11 | - id: check-shebang-scripts-are-executable 12 | - id: check-symlinks 13 | - id: check-yaml 14 | - id: debug-statements 15 | exclude: tests/ 16 | - id: destroyed-symlinks 17 | - id: end-of-file-fixer 18 | exclude: tests/test_changes/ 19 | files: \.(py|sh|rst|yml|yaml)$ 20 | - id: mixed-line-ending 21 | - id: trailing-whitespace 22 | files: \.(py|sh|rst|yml|yaml)$ 23 | - repo: https://github.com/astral-sh/ruff-pre-commit 24 | rev: v0.11.7 25 | hooks: 26 | - id: ruff 27 | args: [ --fix ] 28 | exclude: tests/|anylabeling/resources/resources.py 29 | - id: ruff-format 30 | exclude: tests/|anylabeling/resources/resources.py 31 | - repo: https://github.com/rstcheck/rstcheck 32 | rev: v6.1.2 33 | hooks: 34 | - id: rstcheck 35 | args: [ 36 | --report-level=warning, 37 | ] 38 | - repo: https://github.com/codespell-project/codespell 39 | rev: v2.2.4 40 | hooks: 41 | - id: codespell 42 | files: \.(py|sh|rst|yml|yaml)$ 43 | args: 44 | - --ignore-words-list=datas 45 | - --skip=scripts/build_macos_folder.sh 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Python Module", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "anylabeling.app", 12 | "justMyCode": true 13 | }, 14 | { 15 | "name": "Profile Python Module", 16 | "type": "python", 17 | "request": "launch", 18 | "module": "cProfile", 19 | "args": ["-o", "profile_output.prof", "-m", "anylabeling.app"], 20 | "justMyCode": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Nguyen" 5 | given-names: "Viet Anh" 6 | orcid: https://orcid.org/0009-0002-0457-7811 7 | title: "AnyLabeling - Effortless data labeling with AI support" 8 | url: "https://github.com/vietanhdev/anylabeling" 9 | license: GPL-3 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include anylabeling/configs * 2 | recursive-include anylabeling/views/labeling/icons * 3 | global-include *.ui 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | AnyLabeling 3 |

🌟 AnyLabeling 🌟

4 |

Effortless data labeling with AI support from YOLO and Segment Anything!

5 |

AnyLabeling = LabelImg + Labelme + Improved UI + Auto-labeling

6 |

7 | 8 | ![](https://user-images.githubusercontent.com/18329471/234640541-a6a65fbc-d7a5-4ec3-9b65-55305b01a7aa.png) 9 | 10 | [![PyPI](https://img.shields.io/pypi/v/anylabeling)](https://pypi.org/project/anylabeling) 11 | [![license](https://img.shields.io/github/license/vietanhdev/anylabeling.svg)](https://github.com/vietanhdev/anylabeling/blob/master/LICENSE) 12 | [![open issues](https://isitmaintained.com/badge/open/vietanhdev/anylabeling.svg)](https://github.com/vietanhdev/anylabeling/issues) 13 | [![Pypi Downloads](https://pepy.tech/badge/anylabeling)](https://pypi.org/project/anylabeling/) 14 | [![Documentation](https://img.shields.io/badge/Read-Documentation-green)](https://anylabeling.nrl.ai/) 15 | [![Follow](https://img.shields.io/badge/+Follow-vietanhdev-blue)]([[https://anylabeling.nrl.ai/](https://twitter.com/vietanhdev)](https://twitter.com/vietanhdev)) 16 | 17 | [![AnyLearning-Banner](https://github.com/user-attachments/assets/c2de3534-3e04-439b-bdca-19f6fcb9fc61)](https://anylearning.nrl.ai/) 18 | 19 | [![ai-flow 62b3c222](https://github.com/user-attachments/assets/a47a0eea-ec59-4c59-9733-737b1977e56b)](https://anylearning.nrl.ai/) 20 | 21 | 22 | 23 | AnyLabeling 24 | 25 | 26 | **Auto Labeling with Segment Anything** 27 | 28 | 29 | AnyLabeling-SegmentAnything 30 | 31 | 32 | 33 | - **Youtube Demo:** [https://www.youtube.com/watch?v=5qVJiYNX5Kk](https://www.youtube.com/watch?v=5qVJiYNX5Kk) 34 | - **Documentation:** [https://anylabeling.nrl.ai](https://anylabeling.nrl.ai) 35 | 36 | **Features:** 37 | 38 | - [x] Image annotation for polygon, rectangle, circle, line and point. 39 | - [x] Auto-labeling YOLOv8, Segment Anything (SAM, SAM2). 40 | - [x] Text detection, recognition and KIE (Key Information Extraction) labeling. 41 | - [x] Multiple languages availables: English, Vietnamese, Chinese. 42 | 43 | ## Install and Run 44 | 45 | ### 1. Download and run executable 46 | 47 | - Download and run newest version from [Releases](https://github.com/vietanhdev/anylabeling/releases). 48 | - For MacOS: 49 | - Download the folder mode build (`AnyLabeling-Folder.zip`) from [Releases](https://github.com/vietanhdev/anylabeling/releases) 50 | - See [macOS folder mode instructions](docs/macos_folder_mode.md) for details 51 | 52 | ### Install from Pypi 53 | 54 | - Requirements: Python 3.10+. Recommended: Python 3.12. 55 | - Recommended: [Miniconda/Anaconda](https://docs.conda.io/en/latest/miniconda.html). 56 | 57 | - Create environment: 58 | 59 | ```bash 60 | conda create -n anylabeling python=3.12 61 | conda activate anylabeling 62 | ``` 63 | 64 | - **(For macOS only)** Install PyQt5 using Conda: 65 | 66 | ```bash 67 | conda install -c conda-forge pyqt==5.15.9 68 | ``` 69 | 70 | - Install anylabeling: 71 | 72 | ```bash 73 | pip install anylabeling # or pip install anylabeling-gpu for GPU support 74 | ``` 75 | 76 | - Start labeling: 77 | 78 | ```bash 79 | anylabeling 80 | ``` 81 | 82 | ## Documentation 83 | 84 | **Website:** [https://anylabeling.nrl.ai](https://anylabeling.nrl.ai)/ 85 | 86 | ### Applications 87 | 88 | | **Object Detection** | **Recognition** | **Facial Landmark Detection** | **2D Pose Estimation** | 89 | | :---: | :---: | :---: | :---: | 90 | | | | | | 91 | | **2D Lane Detection** | **OCR** | **Medical Imaging** | **Instance Segmentation** | 92 | | | | | | 93 | | **Image Tagging** | **Rotation** | **And more!** | 94 | | | | Your applications here! | 95 | ## Development 96 | 97 | - Install packages: 98 | 99 | ```bash 100 | pip install -r requirements-dev.txt 101 | # or pip install -r requirements-macos-dev.txt for MacOS 102 | ``` 103 | 104 | - Generate resources: 105 | 106 | ```bash 107 | pyrcc5 -o anylabeling/resources/resources.py anylabeling/resources/resources.qrc 108 | ``` 109 | 110 | - Run app: 111 | 112 | ```bash 113 | python anylabeling/app.py 114 | ``` 115 | 116 | ## Build executable 117 | 118 | - Install PyInstaller: 119 | 120 | ```bash 121 | pip install -r requirements-dev.txt 122 | ``` 123 | 124 | - Build: 125 | 126 | ```bash 127 | bash build_executable.sh 128 | ``` 129 | 130 | - Check the outputs in: `dist/`. 131 | 132 | ## Contribution 133 | 134 | If you want to contribute to **AnyLabeling**, please read [Contribution Guidelines](https://anylabeling.nrl.ai/docs/contribution). 135 | 136 | ## Star history 137 | 138 | [![Star History Chart](https://api.star-history.com/svg?repos=vietanhdev/anylabeling&type=Date)](https://star-history.com/#vietanhdev/anylabeling&Date) 139 | 140 | ## References 141 | 142 | - Labeling UI built with ideas and components from [LabelImg](https://github.com/heartexlabs/labelImg), [LabelMe](https://github.com/wkentaro/labelme). 143 | - Auto-labeling with [Segment Anything Models](https://segment-anything.com/), [MobileSAM](https://github.com/ChaoningZhang/MobileSAM). 144 | - Auto-labeling with [YOLOv8](https://github.com/ultralytics/ultralytics). 145 | - Icons from FlatIcon: [DinosoftLabs](https://www.flaticon.com/free-icons/sun "sun icons"), [Freepik](https://www.flaticon.com/free-icons/moon "moon icons"), [Vectoricons](https://www.flaticon.com/free-icons/system "system icons"), [HideMaru](https://www.flaticon.com/free-icons/ungroup "ungroup icons"). 146 | -------------------------------------------------------------------------------- /anylabeling.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=AnyLabeling 3 | Comment=Effortless data labeling with AI support 4 | Exec=AnyLabeling 5 | Icon=anylabeling/resources/images/logo.png 6 | Terminal=false 7 | Type=Application 8 | Categories=Graphics;RasterGraphics;MachineLearning 9 | -------------------------------------------------------------------------------- /anylabeling.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | # vim: ft=python 3 | 4 | import sys 5 | 6 | sys.setrecursionlimit(5000) # required on Windows 7 | 8 | a = Analysis( 9 | ['anylabeling/app.py'], 10 | pathex=['anylabeling'], 11 | binaries=[], 12 | datas=[ 13 | ('anylabeling/configs/auto_labeling/*.yaml', 'anylabeling/configs/auto_labeling'), 14 | ('anylabeling/configs/*.yaml', 'anylabeling/configs'), 15 | ('anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui', 'anylabeling/views/labeling/widgets/auto_labeling') 16 | ], 17 | hiddenimports=[], 18 | hookspath=[], 19 | runtime_hooks=[], 20 | excludes=[], 21 | ) 22 | pyz = PYZ(a.pure, a.zipped_data) 23 | exe = EXE( 24 | pyz, 25 | a.scripts, 26 | a.binaries, 27 | a.zipfiles, 28 | a.datas, 29 | name='anylabeling', 30 | debug=False, 31 | strip=False, 32 | upx=False, 33 | runtime_tmpdir=None, 34 | console=False, 35 | icon='anylabeling/resources/images/icon.icns', 36 | ) 37 | app = BUNDLE( 38 | exe, 39 | name='AnyLabeling.app', 40 | icon='anylabeling/resources/images/icon.icns', 41 | bundle_identifier=None, 42 | info_plist={'NSHighResolutionCapable': 'True'}, 43 | ) 44 | -------------------------------------------------------------------------------- /anylabeling/README.md: -------------------------------------------------------------------------------- 1 | # Coding structure 2 | 3 | - **models:** Data model classes for PyQt 4 | - **views:** View classes for PyQt 5 | - **services:** Independent services for system interaction 6 | - **common:** Common utilities 7 | - **configs:** Configuration files 8 | - **resources:** Resources for PyQt 9 | -------------------------------------------------------------------------------- /anylabeling/__init__.py: -------------------------------------------------------------------------------- 1 | from .app_info import __appdescription__, __appname__, __version__ 2 | 3 | __all__ = ["__appdescription__", "__appname__", "__version__"] 4 | -------------------------------------------------------------------------------- /anylabeling/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Temporary fix for: bus error 4 | # Source: https://stackoverflow.com/questions/73072612/ 5 | # why-does-np-linalg-solve-raise-bus-error-when-running-on-its-own-thread-mac-m1 6 | os.environ["MKL_NUM_THREADS"] = "1" 7 | os.environ["NUMEXPR_NUM_THREADS"] = "1" 8 | os.environ["OMP_NUM_THREADS"] = "1" 9 | 10 | import argparse 11 | import codecs 12 | import logging 13 | import sys 14 | 15 | import yaml 16 | from PyQt5 import QtCore, QtWidgets 17 | 18 | from anylabeling.app_info import __appname__ 19 | from anylabeling.config import get_config 20 | from anylabeling import config as anylabeling_config 21 | from anylabeling.views.mainwindow import MainWindow 22 | from anylabeling.views.labeling.logger import logger 23 | from anylabeling.views.labeling.utils import new_icon 24 | from anylabeling.resources import resources 25 | from anylabeling.styles import AppTheme 26 | 27 | __all__ = ["resources"] 28 | 29 | 30 | def main(): 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument("--reset-config", action="store_true", help="reset qt config") 33 | parser.add_argument( 34 | "--logger-level", 35 | default="info", 36 | choices=["debug", "info", "warning", "fatal", "error"], 37 | help="logger level", 38 | ) 39 | parser.add_argument("filename", nargs="?", help="image or label filename") 40 | parser.add_argument( 41 | "--output", 42 | "-O", 43 | "-o", 44 | help=( 45 | "output file or directory (if it ends with .json it is " 46 | "recognized as file, else as directory)" 47 | ), 48 | ) 49 | default_config_file = os.path.join(os.path.expanduser("~"), ".anylabelingrc") 50 | parser.add_argument( 51 | "--config", 52 | dest="config", 53 | help=(f"config file or yaml-format string (default: {default_config_file})"), 54 | default=default_config_file, 55 | ) 56 | # config for the gui 57 | parser.add_argument( 58 | "--nodata", 59 | dest="store_data", 60 | action="store_false", 61 | help="stop storing image data to JSON file", 62 | default=argparse.SUPPRESS, 63 | ) 64 | parser.add_argument( 65 | "--autosave", 66 | dest="auto_save", 67 | action="store_true", 68 | help="auto save", 69 | default=argparse.SUPPRESS, 70 | ) 71 | parser.add_argument( 72 | "--nosortlabels", 73 | dest="sort_labels", 74 | action="store_false", 75 | help="stop sorting labels", 76 | default=argparse.SUPPRESS, 77 | ) 78 | parser.add_argument( 79 | "--flags", 80 | help="comma separated list of flags OR file containing flags", 81 | default=argparse.SUPPRESS, 82 | ) 83 | parser.add_argument( 84 | "--labelflags", 85 | dest="label_flags", 86 | help=r"yaml string of label specific flags OR file containing json " 87 | r"string of label specific flags (ex. {person-\d+: [male, tall], " 88 | r"dog-\d+: [black, brown, white], .*: [occluded]})", # NOQA 89 | default=argparse.SUPPRESS, 90 | ) 91 | parser.add_argument( 92 | "--labels", 93 | help="comma separated list of labels OR file containing labels", 94 | default=argparse.SUPPRESS, 95 | ) 96 | parser.add_argument( 97 | "--validatelabel", 98 | dest="validate_label", 99 | choices=["exact"], 100 | help="label validation types", 101 | default=argparse.SUPPRESS, 102 | ) 103 | parser.add_argument( 104 | "--keep-prev", 105 | action="store_true", 106 | help="keep annotation of previous frame", 107 | default=argparse.SUPPRESS, 108 | ) 109 | parser.add_argument( 110 | "--epsilon", 111 | type=float, 112 | help="epsilon to find nearest vertex on canvas", 113 | default=argparse.SUPPRESS, 114 | ) 115 | # Add theme argument 116 | parser.add_argument( 117 | "--theme", 118 | choices=["system", "light", "dark"], 119 | help="set application theme (default: system)", 120 | default="system", 121 | ) 122 | args = parser.parse_args() 123 | 124 | logger.setLevel(getattr(logging, args.logger_level.upper())) 125 | 126 | if hasattr(args, "flags"): 127 | if os.path.isfile(args.flags): 128 | with codecs.open(args.flags, "r", encoding="utf-8") as f: 129 | args.flags = [line.strip() for line in f if line.strip()] 130 | else: 131 | args.flags = [line for line in args.flags.split(",") if line] 132 | 133 | if hasattr(args, "labels"): 134 | if os.path.isfile(args.labels): 135 | with codecs.open(args.labels, "r", encoding="utf-8") as f: 136 | args.labels = [line.strip() for line in f if line.strip()] 137 | else: 138 | args.labels = [line for line in args.labels.split(",") if line] 139 | 140 | if hasattr(args, "label_flags"): 141 | if os.path.isfile(args.label_flags): 142 | with codecs.open(args.label_flags, "r", encoding="utf-8") as f: 143 | args.label_flags = yaml.safe_load(f) 144 | else: 145 | args.label_flags = yaml.safe_load(args.label_flags) 146 | 147 | config_from_args = args.__dict__ 148 | reset_config = config_from_args.pop("reset_config") 149 | filename = config_from_args.pop("filename") 150 | output = config_from_args.pop("output") 151 | config_file_or_yaml = config_from_args.pop("config") 152 | theme = config_from_args.pop("theme") 153 | anylabeling_config.current_config_file = config_file_or_yaml 154 | config = get_config(config_file_or_yaml, config_from_args) 155 | 156 | if not config["labels"] and config["validate_label"]: 157 | logger.error( 158 | "--labels must be specified with --validatelabel or " 159 | "validate_label: true in the config file " 160 | "(ex. ~/.anylabelingrc)." 161 | ) 162 | sys.exit(1) 163 | 164 | output_file = None 165 | output_dir = None 166 | if output is not None: 167 | if output.endswith(".json"): 168 | output_file = output 169 | else: 170 | output_dir = output 171 | 172 | language = config.get("language", QtCore.QLocale.system().name()) 173 | translator = QtCore.QTranslator() 174 | loaded_language = translator.load(":/languages/translations/" + language + ".qm") 175 | 176 | # Enable scaling for high dpi screens 177 | QtWidgets.QApplication.setAttribute( 178 | QtCore.Qt.AA_EnableHighDpiScaling, True 179 | ) # enable highdpi scaling 180 | QtWidgets.QApplication.setAttribute( 181 | QtCore.Qt.AA_UseHighDpiPixmaps, True 182 | ) # use highdpi icons 183 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) 184 | 185 | app = QtWidgets.QApplication(sys.argv) 186 | app.processEvents() 187 | 188 | # Apply theme 189 | if theme != "system": 190 | # Override system theme detection 191 | os.environ["DARK_MODE"] = "1" if theme == "dark" else "0" 192 | else: 193 | # Check if theme is in config 194 | config_theme = config.get("theme", "system") 195 | if config_theme != "system": 196 | os.environ["DARK_MODE"] = "1" if config_theme == "dark" else "0" 197 | 198 | # Apply our modern theme 199 | AppTheme.apply_theme(app) 200 | 201 | app.setApplicationName(__appname__) 202 | app.setWindowIcon(new_icon("icon")) 203 | if loaded_language: 204 | app.installTranslator(translator) 205 | else: 206 | logger.warning( 207 | "Failed to load translation for %s. Using default language.", 208 | language, 209 | ) 210 | win = MainWindow( 211 | app, 212 | config=config, 213 | filename=filename, 214 | output_file=output_file, 215 | output_dir=output_dir, 216 | ) 217 | 218 | if reset_config: 219 | logger.info("Resetting Qt config: %s", win.settings.fileName()) 220 | win.settings.clear() 221 | sys.exit(0) 222 | 223 | win.showMaximized() 224 | win.raise_() 225 | sys.exit(app.exec()) 226 | 227 | 228 | # this main block is required to generate executable by pyinstaller 229 | if __name__ == "__main__": 230 | main() 231 | -------------------------------------------------------------------------------- /anylabeling/app_info.py: -------------------------------------------------------------------------------- 1 | __appname__ = "AnyLabeling" 2 | __appdescription__ = "Effortless data labeling with AI support" 3 | __version__ = "0.4.30" 4 | __preferred_device__ = "CPU" # GPU or CPU 5 | -------------------------------------------------------------------------------- /anylabeling/config.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | 3 | try: 4 | import importlib.resources as pkg_resources 5 | except ImportError: 6 | # Try backported to PY<37 `importlib_resources`. 7 | import importlib_resources as pkg_resources 8 | 9 | import yaml 10 | 11 | from anylabeling import configs as anylabeling_configs 12 | 13 | from .views.labeling.logger import logger 14 | 15 | 16 | # Save current config file 17 | current_config_file = None 18 | 19 | 20 | def update_dict(target_dict, new_dict, validate_item=None): 21 | for key, value in new_dict.items(): 22 | if validate_item: 23 | validate_item(key, value) 24 | # Special handling for recognized new keys 25 | if key not in target_dict and key in ["theme", "ui"]: 26 | target_dict[key] = value 27 | continue 28 | if key not in target_dict: 29 | logger.warning("Skipping unexpected key in config: %s", key) 30 | continue 31 | if isinstance(target_dict[key], dict) and isinstance(value, dict): 32 | update_dict(target_dict[key], value, validate_item=validate_item) 33 | else: 34 | target_dict[key] = value 35 | 36 | 37 | def save_config(config): 38 | # Local config file 39 | user_config_file = osp.join(osp.expanduser("~"), ".anylabelingrc") 40 | try: 41 | with open(user_config_file, "w") as f: 42 | yaml.safe_dump(config, f) 43 | except Exception: # noqa 44 | logger.warning("Failed to save config: %s", user_config_file) 45 | 46 | 47 | def get_default_config(): 48 | config_file = "anylabeling_config.yaml" 49 | with pkg_resources.open_text(anylabeling_configs, config_file) as f: 50 | config = yaml.safe_load(f) 51 | 52 | # Save default config to ~/.anylabelingrc 53 | if not osp.exists(osp.join(osp.expanduser("~"), ".anylabelingrc")): 54 | save_config(config) 55 | 56 | return config 57 | 58 | 59 | def validate_config_item(key, value): 60 | if key == "validate_label" and value not in [None, "exact"]: 61 | raise ValueError(f"Unexpected value for config key 'validate_label': {value}") 62 | if key == "shape_color" and value not in [None, "auto", "manual"]: 63 | raise ValueError(f"Unexpected value for config key 'shape_color': {value}") 64 | if key == "labels" and value is not None and len(value) != len(set(value)): 65 | raise ValueError(f"Duplicates are detected for config key 'labels': {value}") 66 | if key == "theme" and value not in ["system", "light", "dark"]: 67 | raise ValueError(f"Unexpected value for config key 'theme': {value}") 68 | 69 | 70 | def get_config(config_file_or_yaml=None, config_from_args=None): 71 | # 1. default config 72 | config = get_default_config() 73 | 74 | # 2. specified as file or yaml 75 | if config_file_or_yaml is None: 76 | config_file_or_yaml = current_config_file 77 | if config_file_or_yaml is not None: 78 | config_from_yaml = yaml.safe_load(config_file_or_yaml) 79 | if not isinstance(config_from_yaml, dict): 80 | with open(config_from_yaml) as f: 81 | logger.info("Loading config file from: %s", config_from_yaml) 82 | config_from_yaml = yaml.safe_load(f) 83 | update_dict(config, config_from_yaml, validate_item=validate_config_item) 84 | 85 | # 3. command line argument or specified config file 86 | if config_from_args is not None: 87 | update_dict(config, config_from_args, validate_item=validate_config_item) 88 | 89 | return config 90 | -------------------------------------------------------------------------------- /anylabeling/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/configs/__init__.py -------------------------------------------------------------------------------- /anylabeling/configs/anylabeling_config.yaml: -------------------------------------------------------------------------------- 1 | language: en_US 2 | theme: system 3 | auto_save: true 4 | display_label_popup: true 5 | store_data: false 6 | keep_prev: false 7 | keep_prev_scale: false 8 | keep_prev_brightness: false 9 | keep_prev_contrast: false 10 | auto_use_last_label: false 11 | show_cross_line: true 12 | show_groups: true 13 | show_texts: true 14 | logger_level: info 15 | 16 | flags: null 17 | label_flags: null 18 | labels: null 19 | file_search: null 20 | sort_labels: true 21 | validate_label: null 22 | 23 | default_shape_color: [0, 255, 0] 24 | shape_color: auto # null, 'auto', 'manual' 25 | shift_auto_shape_color: 0 26 | label_colors: null 27 | 28 | shape: 29 | # drawing 30 | line_color: [0, 255, 0, 128] 31 | fill_color: [220, 220, 220, 150] 32 | vertex_fill_color: [0, 255, 0, 255] 33 | # selecting / hovering 34 | select_line_color: [255, 255, 255, 255] 35 | select_fill_color: [0, 255, 0, 155] 36 | hvertex_fill_color: [255, 255, 255, 255] 37 | point_size: 8 38 | 39 | # main 40 | flag_dock: 41 | show: true 42 | closable: false 43 | movable: false 44 | floatable: false 45 | label_dock: 46 | show: true 47 | closable: false 48 | movable: false 49 | floatable: false 50 | shape_dock: 51 | show: true 52 | closable: false 53 | movable: false 54 | floatable: false 55 | file_dock: 56 | show: true 57 | closable: false 58 | movable: false 59 | floatable: false 60 | 61 | # label_dialog 62 | show_label_text_field: true 63 | label_completion: startswith 64 | fit_to_content: 65 | column: true 66 | row: false 67 | 68 | # canvas 69 | epsilon: 10.0 70 | canvas: 71 | # None: do nothing 72 | # close: close polygon 73 | double_click: close 74 | # The max number of edits we can undo 75 | num_backups: 10 76 | 77 | shortcuts: 78 | close: Ctrl+W 79 | open: Ctrl+O 80 | open_dir: Ctrl+U 81 | quit: Ctrl+Q 82 | save: Ctrl+S 83 | save_as: Ctrl+Shift+S 84 | save_to: null 85 | delete_file: Ctrl+Delete 86 | 87 | open_next: [D, Ctrl+Shift+D] 88 | open_prev: [A, Ctrl+Shift+A] 89 | 90 | zoom_in: [Ctrl++, Ctrl+=] 91 | zoom_out: Ctrl+- 92 | zoom_to_original: Ctrl+0 93 | fit_window: Ctrl+F 94 | fit_width: Ctrl+Shift+F 95 | 96 | create_polygon: [P, Ctrl+N] 97 | create_rectangle: [R, Ctrl+R] 98 | create_circle: null 99 | create_line: null 100 | create_point: null 101 | create_linestrip: null 102 | edit_polygon: Ctrl+J 103 | delete_polygon: Delete 104 | duplicate_polygon: Ctrl+D 105 | copy_polygon: Ctrl+C 106 | paste_polygon: Ctrl+V 107 | undo: Ctrl+Z 108 | undo_last_point: Ctrl+Z 109 | add_point_to_edge: Ctrl+Shift+P 110 | edit_label: Ctrl+E 111 | toggle_keep_prev_mode: Ctrl+P 112 | remove_selected_point: Backspace 113 | group_selected_shapes: G 114 | ungroup_selected_shapes: U 115 | toggle_auto_use_last_label: Ctrl+Y 116 | 117 | auto_label: Ctrt+A 118 | 119 | 120 | # Auto labeling 121 | custom_models: [] 122 | 123 | # UI settings 124 | ui: 125 | dock_state: null 126 | -------------------------------------------------------------------------------- /anylabeling/configs/auto_labeling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/configs/auto_labeling/__init__.py -------------------------------------------------------------------------------- /anylabeling/configs/auto_labeling/models.yaml: -------------------------------------------------------------------------------- 1 | - name: "sam2_hiera_tiny_20240803" 2 | display_name: Segment Anything 2 (Hiera-Tiny) 3 | download_url: https://huggingface.co/vietanhdev/segment-anything-2-onnx-models/resolve/main/sam2_hiera_tiny.zip 4 | - name: "sam2_hiera_small_20240803" 5 | display_name: Segment Anything 2 (Hiera-Small) 6 | download_url: https://huggingface.co/vietanhdev/segment-anything-2-onnx-models/resolve/main/sam2_hiera_small.zip 7 | - name: "sam2_hiera_base_plus_20240803" 8 | display_name: Segment Anything 2 (Hiera-Base+) 9 | download_url: https://huggingface.co/vietanhdev/segment-anything-2-onnx-models/resolve/main/sam2_hiera_base_plus.zip 10 | - name: "sam2_hiera_large_20240803" 11 | display_name: Segment Anything 2 (Hiera-Large) 12 | download_url: https://huggingface.co/vietanhdev/segment-anything-2-onnx-models/resolve/main/sam2_hiera_large.zip 13 | - name: "mobile_sam_20230629" 14 | display_name: Segment Anything (MobileSAM) 15 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/mobile_sam_20230629.zip 16 | - name: "sam_vit_b_01ec64" 17 | display_name: Segment Anything (ViT-B) 18 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_b_01ec64.zip 19 | - name: "sam_vit_b_01ec64_quant" 20 | display_name: Segment Anything (ViT-B Quant) 21 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_b_01ec64_quant.zip 22 | - name: "sam_vit_l_0b3195" 23 | display_name: Segment Anything (ViT-L) 24 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_l_0b3195.zip 25 | - name: "sam_vit_l_0b3195_quant" 26 | display_name: Segment Anything (ViT-L Quant) 27 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_l_0b3195_quant.zip 28 | - name: "sam_vit_h_4b8939" 29 | display_name: Segment Anything (ViT-H) 30 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_h_4b8939.zip 31 | - name: "sam_vit_h_4b8939_quant" 32 | display_name: Segment Anything (ViT-H Quant) 33 | download_url: https://huggingface.co/vietanhdev/segment-anything-onnx-models/resolve/main/sam_vit_h_4b8939_quant.zip 34 | - name: "yolov8n-r20230415" 35 | display_name: YOLOv8n 36 | download_url: https://github.com/vietanhdev/anylabeling-assets/releases/download/v0.4.0/yolov8n-r20230415.zip 37 | - name: "yolov8s-r20230415" 38 | display_name: YOLOv8s 39 | download_url: https://github.com/vietanhdev/anylabeling-assets/releases/download/v0.4.0/yolov8s-r20230415.zip 40 | - name: "yolov8m-r20230415" 41 | display_name: YOLOv8m 42 | download_url: https://github.com/vietanhdev/anylabeling-assets/releases/download/v0.4.0/yolov8m-r20230415.zip 43 | - name: "yolov8l-r20230415" 44 | display_name: YOLOv8l 45 | download_url: https://github.com/vietanhdev/anylabeling-assets/releases/download/v0.4.0/yolov8l-r20230415.zip 46 | - name: "yolov8x-r20230415" 47 | display_name: YOLOv8x 48 | download_url: https://github.com/vietanhdev/anylabeling-assets/releases/download/v0.4.0/yolov8x-r20230415.zip 49 | -------------------------------------------------------------------------------- /anylabeling/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/__init__.py -------------------------------------------------------------------------------- /anylabeling/resources/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /anylabeling/resources/icons/tools.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /anylabeling/resources/images/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/box.png -------------------------------------------------------------------------------- /anylabeling/resources/images/brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/brain.png -------------------------------------------------------------------------------- /anylabeling/resources/images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/cancel.png -------------------------------------------------------------------------------- /anylabeling/resources/images/cartesian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/cartesian.png -------------------------------------------------------------------------------- /anylabeling/resources/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/check.png -------------------------------------------------------------------------------- /anylabeling/resources/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/circle.png -------------------------------------------------------------------------------- /anylabeling/resources/images/cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/cn.png -------------------------------------------------------------------------------- /anylabeling/resources/images/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/color.png -------------------------------------------------------------------------------- /anylabeling/resources/images/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/computer.png -------------------------------------------------------------------------------- /anylabeling/resources/images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/copy.png -------------------------------------------------------------------------------- /anylabeling/resources/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/delete.png -------------------------------------------------------------------------------- /anylabeling/resources/images/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/done.png -------------------------------------------------------------------------------- /anylabeling/resources/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/edit.png -------------------------------------------------------------------------------- /anylabeling/resources/images/expert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/expert.png -------------------------------------------------------------------------------- /anylabeling/resources/images/expert1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/expert1.png -------------------------------------------------------------------------------- /anylabeling/resources/images/expert2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/expert2.png -------------------------------------------------------------------------------- /anylabeling/resources/images/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/eye.png -------------------------------------------------------------------------------- /anylabeling/resources/images/feBlend-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/feBlend-icon.png -------------------------------------------------------------------------------- /anylabeling/resources/images/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/file.png -------------------------------------------------------------------------------- /anylabeling/resources/images/fit-width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/fit-width.png -------------------------------------------------------------------------------- /anylabeling/resources/images/fit-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/fit-window.png -------------------------------------------------------------------------------- /anylabeling/resources/images/fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/fit.png -------------------------------------------------------------------------------- /anylabeling/resources/images/format_createml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/format_createml.png -------------------------------------------------------------------------------- /anylabeling/resources/images/format_voc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/format_voc.png -------------------------------------------------------------------------------- /anylabeling/resources/images/format_yolo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/format_yolo.png -------------------------------------------------------------------------------- /anylabeling/resources/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/group.png -------------------------------------------------------------------------------- /anylabeling/resources/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/help.png -------------------------------------------------------------------------------- /anylabeling/resources/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/icon.icns -------------------------------------------------------------------------------- /anylabeling/resources/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/icon.ico -------------------------------------------------------------------------------- /anylabeling/resources/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/icon.png -------------------------------------------------------------------------------- /anylabeling/resources/images/labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/labels.png -------------------------------------------------------------------------------- /anylabeling/resources/images/line-strip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/line-strip.png -------------------------------------------------------------------------------- /anylabeling/resources/images/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/line.png -------------------------------------------------------------------------------- /anylabeling/resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/logo.png -------------------------------------------------------------------------------- /anylabeling/resources/images/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/minus.png -------------------------------------------------------------------------------- /anylabeling/resources/images/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/moon.png -------------------------------------------------------------------------------- /anylabeling/resources/images/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/new.png -------------------------------------------------------------------------------- /anylabeling/resources/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/next.png -------------------------------------------------------------------------------- /anylabeling/resources/images/objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/objects.png -------------------------------------------------------------------------------- /anylabeling/resources/images/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/open.png -------------------------------------------------------------------------------- /anylabeling/resources/images/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/paste.png -------------------------------------------------------------------------------- /anylabeling/resources/images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/plus.png -------------------------------------------------------------------------------- /anylabeling/resources/images/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/point.png -------------------------------------------------------------------------------- /anylabeling/resources/images/polygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/polygon.png -------------------------------------------------------------------------------- /anylabeling/resources/images/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/prev.png -------------------------------------------------------------------------------- /anylabeling/resources/images/quit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/quit.png -------------------------------------------------------------------------------- /anylabeling/resources/images/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/rectangle.png -------------------------------------------------------------------------------- /anylabeling/resources/images/resetall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/resetall.png -------------------------------------------------------------------------------- /anylabeling/resources/images/save-as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/save-as.png -------------------------------------------------------------------------------- /anylabeling/resources/images/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/save.png -------------------------------------------------------------------------------- /anylabeling/resources/images/scissors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/scissors.png -------------------------------------------------------------------------------- /anylabeling/resources/images/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/sun.png -------------------------------------------------------------------------------- /anylabeling/resources/images/undo-cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/undo-cross.png -------------------------------------------------------------------------------- /anylabeling/resources/images/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/undo.png -------------------------------------------------------------------------------- /anylabeling/resources/images/upload_brain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/upload_brain.png -------------------------------------------------------------------------------- /anylabeling/resources/images/us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/us.png -------------------------------------------------------------------------------- /anylabeling/resources/images/verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/verify.png -------------------------------------------------------------------------------- /anylabeling/resources/images/vn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/vn.png -------------------------------------------------------------------------------- /anylabeling/resources/images/zoom-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/zoom-in.png -------------------------------------------------------------------------------- /anylabeling/resources/images/zoom-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/zoom-out.png -------------------------------------------------------------------------------- /anylabeling/resources/images/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/images/zoom.png -------------------------------------------------------------------------------- /anylabeling/resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | images/minus.png 4 | images/plus.png 5 | images/box.png 6 | images/cancel.png 7 | images/check.png 8 | images/edit.png 9 | images/group.png 10 | images/next.png 11 | images/open.png 12 | images/prev.png 13 | images/save.png 14 | images/brain.png 15 | images/cartesian.png 16 | images/circle.png 17 | images/cn.png 18 | images/color.png 19 | images/computer.png 20 | images/copy.png 21 | images/delete.png 22 | images/done.png 23 | images/expert.png 24 | images/expert1.png 25 | images/expert2.png 26 | images/eye.png 27 | images/feBlend-icon.png 28 | images/file.png 29 | images/fit-width.png 30 | images/fit-window.png 31 | images/fit.png 32 | images/format_createml.png 33 | images/format_voc.png 34 | images/format_yolo.png 35 | images/help.png 36 | images/icon.icns 37 | images/icon.ico 38 | images/icon.png 39 | images/labels.png 40 | images/line-strip.png 41 | images/line.png 42 | images/logo.png 43 | images/moon.png 44 | images/new.png 45 | images/objects.png 46 | images/paste.png 47 | images/point.png 48 | images/polygon.png 49 | images/quit.png 50 | images/rectangle.png 51 | images/resetall.png 52 | images/save-as.png 53 | images/scissors.png 54 | images/sun.png 55 | images/undo-cross.png 56 | images/undo.png 57 | images/upload_brain.png 58 | images/us.png 59 | images/verify.png 60 | images/vn.png 61 | images/zoom-in.png 62 | images/zoom-out.png 63 | images/zoom.png 64 | 65 | 66 | translations/en_US.qm 67 | translations/vi_VN.qm 68 | translations/zh_CN.qm 69 | 70 | 71 | -------------------------------------------------------------------------------- /anylabeling/resources/translations/en_US.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/translations/en_US.qm -------------------------------------------------------------------------------- /anylabeling/resources/translations/vi_VN.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/translations/vi_VN.qm -------------------------------------------------------------------------------- /anylabeling/resources/translations/zh_CN.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/resources/translations/zh_CN.qm -------------------------------------------------------------------------------- /anylabeling/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/services/__init__.py -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/services/auto_labeling/__init__.py -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/lru_cache.py: -------------------------------------------------------------------------------- 1 | """Thread-safe LRU cache implementation.""" 2 | 3 | from collections import OrderedDict 4 | import threading 5 | 6 | 7 | class LRUCache: 8 | """Thread-safe LRU cache implementation.""" 9 | 10 | def __init__(self, maxsize=10): 11 | self.maxsize = maxsize 12 | self.lock = threading.Lock() 13 | self._cache = OrderedDict() 14 | 15 | def get(self, key): 16 | """Get value from cache. Returns None if key is not present.""" 17 | with self.lock: 18 | if key not in self._cache: 19 | return None 20 | self._cache.move_to_end(key) 21 | return self._cache[key] 22 | 23 | def put(self, key, value): 24 | """Put value into cache. If cache is full, oldest item is evicted.""" 25 | with self.lock: 26 | self._cache[key] = value 27 | self._cache.move_to_end(key) 28 | if len(self._cache) > self.maxsize: 29 | self._cache.popitem(last=False) 30 | 31 | def find(self, key): 32 | """Returns True if key is in cache, False otherwise.""" 33 | with self.lock: 34 | return key in self._cache 35 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/model.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import yaml 4 | import socket 5 | import ssl 6 | from abc import abstractmethod 7 | 8 | from PyQt5.QtCore import QCoreApplication, QFile, QObject 9 | from PyQt5.QtGui import QImage 10 | 11 | from .types import AutoLabelingResult 12 | from anylabeling.views.labeling.label_file import LabelFile, LabelFileError 13 | 14 | # Prevent issue when downloading models behind a proxy 15 | os.environ["no_proxy"] = "*" 16 | 17 | socket.setdefaulttimeout(240) # Prevent timeout when downloading models 18 | 19 | 20 | ssl._create_default_https_context = ( 21 | ssl._create_unverified_context 22 | ) # Prevent issue when downloading models behind a proxy 23 | 24 | 25 | class Model(QObject): 26 | BASE_DOWNLOAD_URL = "https://github.com/vietanhdev/anylabeling-assets/raw/main/" 27 | 28 | class Meta(QObject): 29 | required_config_names = [] 30 | widgets = ["button_run"] 31 | output_modes = { 32 | "rectangle": QCoreApplication.translate("Model", "Rectangle"), 33 | } 34 | default_output_mode = "rectangle" 35 | 36 | def __init__(self, model_config, on_message) -> None: 37 | super().__init__() 38 | self.on_message = on_message 39 | # Load and check config 40 | if isinstance(model_config, str): 41 | if not os.path.isfile(model_config): 42 | raise FileNotFoundError( 43 | QCoreApplication.translate( 44 | "Model", "Config file not found: {model_config}" 45 | ).format(model_config=model_config) 46 | ) 47 | with open(model_config, "r") as f: 48 | self.config = yaml.safe_load(f) 49 | elif isinstance(model_config, dict): 50 | self.config = model_config 51 | else: 52 | raise ValueError( 53 | QCoreApplication.translate( 54 | "Model", "Unknown config type: {type}" 55 | ).format(type=type(model_config)) 56 | ) 57 | self.check_missing_config( 58 | config_names=self.Meta.required_config_names, 59 | config=self.config, 60 | ) 61 | self.output_mode = self.Meta.default_output_mode 62 | 63 | def get_required_widgets(self): 64 | """ 65 | Get required widgets for showing in UI 66 | """ 67 | return self.Meta.widgets 68 | 69 | def get_model_abs_path(self, model_config, model_path_field_name): 70 | """ 71 | Get model absolute path from config path or download from url 72 | """ 73 | # Try getting model path from config folder 74 | config_folder = os.path.dirname(model_config["config_file"]) 75 | model_path = model_config[model_path_field_name] 76 | if os.path.isfile(os.path.join(config_folder, model_path)): 77 | model_abs_path = os.path.abspath(os.path.join(config_folder, model_path)) 78 | return model_abs_path 79 | 80 | # Try getting model from assets folder 81 | home_dir = os.path.expanduser("~") 82 | model_abs_path = os.path.abspath( 83 | os.path.join( 84 | home_dir, 85 | "anylabeling_data", 86 | "models", 87 | model_config["name"], 88 | model_path, 89 | ) 90 | ) 91 | return model_abs_path 92 | 93 | def check_missing_config(self, config_names, config): 94 | """ 95 | Check if config has all required config names 96 | """ 97 | for name in config_names: 98 | if name not in config: 99 | raise Exception(f"Missing config: {name}") 100 | 101 | @abstractmethod 102 | def predict_shapes(self, image, filename=None) -> AutoLabelingResult: 103 | """ 104 | Predict image and return AnyLabeling shapes 105 | """ 106 | raise NotImplementedError 107 | 108 | @abstractmethod 109 | def unload(self): 110 | """ 111 | Unload memory 112 | """ 113 | raise NotImplementedError 114 | 115 | @staticmethod 116 | def load_image_from_filename(filename): 117 | """Load image from labeling file and return image data and image path.""" 118 | label_file = os.path.splitext(filename)[0] + ".json" 119 | if QFile.exists(label_file) and LabelFile.is_label_file(label_file): 120 | try: 121 | label_file = LabelFile(label_file) 122 | except LabelFileError as e: 123 | logging.error("Error reading {}: {}".format(label_file, e)) 124 | return None, None 125 | image_data = label_file.image_data 126 | else: 127 | image_data = LabelFile.load_image_file(filename) 128 | image = QImage.fromData(image_data) 129 | if image.isNull(): 130 | logging.error("Error reading {}".format(filename)) 131 | return image 132 | 133 | def on_next_files_changed(self, next_files): 134 | """ 135 | Handle next files changed. This function can preload next files 136 | and run inference to save time for user. 137 | """ 138 | pass 139 | 140 | def set_output_mode(self, mode): 141 | """ 142 | Set output mode 143 | """ 144 | self.output_mode = mode 145 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/sam2_onnx.py: -------------------------------------------------------------------------------- 1 | # Code from: 2 | # https://github.com/vietanhdev/samexporter/blob/main/samexporter/sam2_onnx.py 3 | import time 4 | from typing import Any 5 | 6 | import cv2 7 | import numpy as np 8 | import onnxruntime 9 | from numpy import ndarray 10 | 11 | 12 | class SegmentAnything2ONNX: 13 | """Segmentation model using Segment Anything 2 (SAM2)""" 14 | 15 | def __init__(self, encoder_model_path, decoder_model_path) -> None: 16 | self.encoder = SAM2ImageEncoder(encoder_model_path) 17 | self.decoder = SAM2ImageDecoder( 18 | decoder_model_path, self.encoder.input_shape[2:] 19 | ) 20 | 21 | def encode(self, cv_image: np.ndarray) -> list[np.ndarray]: 22 | original_size = cv_image.shape[:2] 23 | high_res_feats_0, high_res_feats_1, image_embed = self.encoder(cv_image) 24 | return { 25 | "high_res_feats_0": high_res_feats_0, 26 | "high_res_feats_1": high_res_feats_1, 27 | "image_embedding": image_embed, 28 | "original_size": original_size, 29 | } 30 | 31 | def predict_masks(self, embedding, prompt) -> list[np.ndarray]: 32 | points = [] 33 | labels = [] 34 | for mark in prompt: 35 | if mark["type"] == "point": 36 | points.append(mark["data"]) 37 | labels.append(mark["label"]) 38 | elif mark["type"] == "rectangle": 39 | # Add top left point 40 | points.append([mark["data"][0], mark["data"][1]]) 41 | points.append([mark["data"][2], mark["data"][3]]) # bottom right 42 | labels.append(2) 43 | labels.append(3) 44 | points, labels = np.array(points), np.array(labels) 45 | 46 | image_embedding = embedding["image_embedding"] 47 | high_res_feats_0 = embedding["high_res_feats_0"] 48 | high_res_feats_1 = embedding["high_res_feats_1"] 49 | original_size = embedding["original_size"] 50 | self.decoder.set_image_size(original_size) 51 | masks, _ = self.decoder( 52 | image_embedding, 53 | high_res_feats_0, 54 | high_res_feats_1, 55 | points, 56 | labels, 57 | ) 58 | 59 | return masks 60 | 61 | def transform_masks(self, masks, original_size, transform_matrix): 62 | """Transform the masks back to the original image size.""" 63 | output_masks = [] 64 | for batch in range(masks.shape[0]): 65 | batch_masks = [] 66 | for mask_id in range(masks.shape[1]): 67 | mask = masks[batch, mask_id] 68 | mask = cv2.warpAffine( 69 | mask, 70 | transform_matrix[:2], 71 | (original_size[1], original_size[0]), 72 | flags=cv2.INTER_LINEAR, 73 | ) 74 | batch_masks.append(mask) 75 | output_masks.append(batch_masks) 76 | return np.array(output_masks) 77 | 78 | 79 | class SAM2ImageEncoder: 80 | def __init__(self, path: str) -> None: 81 | # Initialize model 82 | self.session = onnxruntime.InferenceSession( 83 | path, providers=onnxruntime.get_available_providers() 84 | ) 85 | 86 | # Get model info 87 | self.get_input_details() 88 | self.get_output_details() 89 | 90 | def __call__(self, image: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 91 | return self.encode_image(image) 92 | 93 | def encode_image( 94 | self, image: np.ndarray 95 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 96 | input_tensor = self.prepare_input(image) 97 | 98 | outputs = self.infer(input_tensor) 99 | 100 | return self.process_output(outputs) 101 | 102 | def prepare_input(self, image: np.ndarray) -> np.ndarray: 103 | self.img_height, self.img_width = image.shape[:2] 104 | 105 | input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 106 | input_img = cv2.resize(input_img, (self.input_width, self.input_height)) 107 | 108 | mean = np.array([0.485, 0.456, 0.406]) 109 | std = np.array([0.229, 0.224, 0.225]) 110 | input_img = (input_img / 255.0 - mean) / std 111 | input_img = input_img.transpose(2, 0, 1) 112 | input_tensor = input_img[np.newaxis, :, :, :].astype(np.float32) 113 | 114 | return input_tensor 115 | 116 | def infer(self, input_tensor: np.ndarray) -> list[np.ndarray]: 117 | start = time.perf_counter() 118 | outputs = self.session.run( 119 | self.output_names, {self.input_names[0]: input_tensor} 120 | ) 121 | 122 | print(f"infer time: {(time.perf_counter() - start) * 1000:.2f} ms") 123 | return outputs 124 | 125 | def process_output( 126 | self, outputs: list[np.ndarray] 127 | ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: 128 | return outputs[0], outputs[1], outputs[2] 129 | 130 | def get_input_details(self) -> None: 131 | model_inputs = self.session.get_inputs() 132 | self.input_names = [model_inputs[i].name for i in range(len(model_inputs))] 133 | 134 | self.input_shape = model_inputs[0].shape 135 | self.input_height = self.input_shape[2] 136 | self.input_width = self.input_shape[3] 137 | 138 | def get_output_details(self) -> None: 139 | model_outputs = self.session.get_outputs() 140 | self.output_names = [model_outputs[i].name for i in range(len(model_outputs))] 141 | 142 | 143 | class SAM2ImageDecoder: 144 | def __init__( 145 | self, 146 | path: str, 147 | encoder_input_size: tuple[int, int], 148 | orig_im_size: tuple[int, int] = None, 149 | mask_threshold: float = 0.0, 150 | ) -> None: 151 | # Initialize model 152 | self.session = onnxruntime.InferenceSession( 153 | path, providers=onnxruntime.get_available_providers() 154 | ) 155 | 156 | self.orig_im_size = ( 157 | orig_im_size if orig_im_size is not None else encoder_input_size 158 | ) 159 | self.encoder_input_size = encoder_input_size 160 | self.mask_threshold = mask_threshold 161 | self.scale_factor = 4 162 | 163 | # Get model info 164 | self.get_input_details() 165 | self.get_output_details() 166 | 167 | def __call__( 168 | self, 169 | image_embed: np.ndarray, 170 | high_res_feats_0: np.ndarray, 171 | high_res_feats_1: np.ndarray, 172 | point_coords: list[np.ndarray] | np.ndarray, 173 | point_labels: list[np.ndarray] | np.ndarray, 174 | ) -> tuple[list[np.ndarray], ndarray]: 175 | return self.predict( 176 | image_embed, 177 | high_res_feats_0, 178 | high_res_feats_1, 179 | point_coords, 180 | point_labels, 181 | ) 182 | 183 | def predict( 184 | self, 185 | image_embed: np.ndarray, 186 | high_res_feats_0: np.ndarray, 187 | high_res_feats_1: np.ndarray, 188 | point_coords: list[np.ndarray] | np.ndarray, 189 | point_labels: list[np.ndarray] | np.ndarray, 190 | ) -> tuple[list[np.ndarray], ndarray]: 191 | inputs = self.prepare_inputs( 192 | image_embed, 193 | high_res_feats_0, 194 | high_res_feats_1, 195 | point_coords, 196 | point_labels, 197 | ) 198 | 199 | outputs = self.infer(inputs) 200 | 201 | return self.process_output(outputs) 202 | 203 | def prepare_inputs( 204 | self, 205 | image_embed: np.ndarray, 206 | high_res_feats_0: np.ndarray, 207 | high_res_feats_1: np.ndarray, 208 | point_coords: list[np.ndarray] | np.ndarray, 209 | point_labels: list[np.ndarray] | np.ndarray, 210 | ): 211 | input_point_coords, input_point_labels = self.prepare_points( 212 | point_coords, point_labels 213 | ) 214 | 215 | num_labels = input_point_labels.shape[0] 216 | mask_input = np.zeros( 217 | ( 218 | num_labels, 219 | 1, 220 | self.encoder_input_size[0] // self.scale_factor, 221 | self.encoder_input_size[1] // self.scale_factor, 222 | ), 223 | dtype=np.float32, 224 | ) 225 | has_mask_input = np.array([0], dtype=np.float32) 226 | 227 | return ( 228 | image_embed, 229 | high_res_feats_0, 230 | high_res_feats_1, 231 | input_point_coords, 232 | input_point_labels, 233 | mask_input, 234 | has_mask_input, 235 | ) 236 | 237 | def prepare_points( 238 | self, 239 | point_coords: list[np.ndarray] | np.ndarray, 240 | point_labels: list[np.ndarray] | np.ndarray, 241 | ) -> tuple[np.ndarray, np.ndarray]: 242 | if isinstance(point_coords, np.ndarray): 243 | input_point_coords = point_coords[np.newaxis, ...] 244 | input_point_labels = point_labels[np.newaxis, ...] 245 | else: 246 | # Find the maximum number of points across all inputs 247 | max_num_points = max([coords.shape[0] for coords in point_coords]) 248 | # We need to make sure that all inputs have the same number of points 249 | # Add invalid points to pad the input (0, 0) with -1 value for labels 250 | input_point_coords = np.zeros( 251 | (len(point_coords), max_num_points, 2), dtype=np.float32 252 | ) 253 | input_point_labels = ( 254 | np.ones((len(point_coords), max_num_points), dtype=np.float32) * -1 255 | ) 256 | 257 | for i, (coords, labels) in enumerate(zip(point_coords, point_labels)): 258 | input_point_coords[i, : coords.shape[0], :] = coords 259 | input_point_labels[i, : labels.shape[0]] = labels 260 | 261 | input_point_coords[..., 0] = ( 262 | input_point_coords[..., 0] 263 | / self.orig_im_size[1] 264 | * self.encoder_input_size[1] 265 | ) # Normalize x 266 | input_point_coords[..., 1] = ( 267 | input_point_coords[..., 1] 268 | / self.orig_im_size[0] 269 | * self.encoder_input_size[0] 270 | ) # Normalize y 271 | 272 | return input_point_coords.astype(np.float32), input_point_labels.astype( 273 | np.float32 274 | ) 275 | 276 | def infer(self, inputs) -> list[np.ndarray]: 277 | start = time.perf_counter() 278 | 279 | outputs = self.session.run( 280 | self.output_names, 281 | {self.input_names[i]: inputs[i] for i in range(len(self.input_names))}, 282 | ) 283 | 284 | print(f"infer time: {(time.perf_counter() - start) * 1000:.2f} ms") 285 | return outputs 286 | 287 | def process_output( 288 | self, outputs: list[np.ndarray] 289 | ) -> tuple[list[ndarray | Any], ndarray[Any, Any]]: 290 | scores = outputs[1].squeeze() 291 | masks = outputs[0][0] 292 | 293 | # Select the best masks based on the scores 294 | best_mask = masks[np.argmax(scores)] 295 | best_mask = cv2.resize(best_mask, (self.orig_im_size[1], self.orig_im_size[0])) 296 | return ( 297 | np.array([[best_mask]]), 298 | scores, 299 | ) 300 | 301 | def set_image_size(self, orig_im_size: tuple[int, int]) -> None: 302 | self.orig_im_size = orig_im_size 303 | 304 | def get_input_details(self) -> None: 305 | model_inputs = self.session.get_inputs() 306 | self.input_names = [model_inputs[i].name for i in range(len(model_inputs))] 307 | 308 | def get_output_details(self) -> None: 309 | model_outputs = self.session.get_outputs() 310 | self.output_names = [model_outputs[i].name for i in range(len(model_outputs))] 311 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/sam_onnx.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import cv2 4 | import numpy as np 5 | import onnxruntime 6 | 7 | 8 | class SegmentAnythingONNX: 9 | """Segmentation model using SegmentAnything""" 10 | 11 | def __init__(self, encoder_model_path, decoder_model_path) -> None: 12 | self.target_size = 1024 13 | self.input_size = (684, 1024) 14 | 15 | self.encoder_session = onnxruntime.InferenceSession(encoder_model_path) 16 | self.encoder_input_name = self.encoder_session.get_inputs()[0].name 17 | self.decoder_session = onnxruntime.InferenceSession(decoder_model_path) 18 | 19 | def get_input_points(self, prompt): 20 | """Get input points""" 21 | points = [] 22 | labels = [] 23 | for mark in prompt: 24 | if mark["type"] == "point": 25 | points.append(mark["data"]) 26 | labels.append(mark["label"]) 27 | elif mark["type"] == "rectangle": 28 | points.append([mark["data"][0], mark["data"][1]]) # top left 29 | points.append([mark["data"][2], mark["data"][3]]) # bottom right 30 | labels.append(2) 31 | labels.append(3) 32 | points, labels = np.array(points), np.array(labels) 33 | return points, labels 34 | 35 | def run_encoder(self, encoder_inputs): 36 | """Run encoder""" 37 | output = self.encoder_session.run(None, encoder_inputs) 38 | image_embedding = output[0] 39 | return image_embedding 40 | 41 | @staticmethod 42 | def get_preprocess_shape(oldh: int, oldw: int, long_side_length: int): 43 | """ 44 | Compute the output size given input size and target long side length. 45 | """ 46 | scale = long_side_length * 1.0 / max(oldh, oldw) 47 | newh, neww = oldh * scale, oldw * scale 48 | neww = int(neww + 0.5) 49 | newh = int(newh + 0.5) 50 | return (newh, neww) 51 | 52 | def apply_coords(self, coords: np.ndarray, original_size, target_length): 53 | """ 54 | Expects a numpy array of length 2 in the final dimension. Requires the 55 | original image size in (H, W) format. 56 | """ 57 | old_h, old_w = original_size 58 | new_h, new_w = self.get_preprocess_shape( 59 | original_size[0], original_size[1], target_length 60 | ) 61 | coords = deepcopy(coords).astype(float) 62 | coords[..., 0] = coords[..., 0] * (new_w / old_w) 63 | coords[..., 1] = coords[..., 1] * (new_h / old_h) 64 | return coords 65 | 66 | def run_decoder(self, image_embedding, original_size, transform_matrix, prompt): 67 | """Run decoder""" 68 | input_points, input_labels = self.get_input_points(prompt) 69 | 70 | # Add a batch index, concatenate a padding point, and transform. 71 | onnx_coord = np.concatenate([input_points, np.array([[0.0, 0.0]])], axis=0)[ 72 | None, :, : 73 | ] 74 | onnx_label = np.concatenate([input_labels, np.array([-1])], axis=0)[ 75 | None, : 76 | ].astype(np.float32) 77 | onnx_coord = self.apply_coords( 78 | onnx_coord, self.input_size, self.target_size 79 | ).astype(np.float32) 80 | 81 | # Apply the transformation matrix to the coordinates. 82 | onnx_coord = np.concatenate( 83 | [ 84 | onnx_coord, 85 | np.ones((1, onnx_coord.shape[1], 1), dtype=np.float32), 86 | ], 87 | axis=2, 88 | ) 89 | onnx_coord = np.matmul(onnx_coord, transform_matrix.T) 90 | onnx_coord = onnx_coord[:, :, :2].astype(np.float32) 91 | 92 | # Create an empty mask input and an indicator for no mask. 93 | onnx_mask_input = np.zeros((1, 1, 256, 256), dtype=np.float32) 94 | onnx_has_mask_input = np.zeros(1, dtype=np.float32) 95 | 96 | decoder_inputs = { 97 | "image_embeddings": image_embedding, 98 | "point_coords": onnx_coord, 99 | "point_labels": onnx_label, 100 | "mask_input": onnx_mask_input, 101 | "has_mask_input": onnx_has_mask_input, 102 | "orig_im_size": np.array(self.input_size, dtype=np.float32), 103 | } 104 | masks, _, _ = self.decoder_session.run(None, decoder_inputs) 105 | 106 | # Transform the masks back to the original image size. 107 | inv_transform_matrix = np.linalg.inv(transform_matrix) 108 | transformed_masks = self.transform_masks( 109 | masks, original_size, inv_transform_matrix 110 | ) 111 | 112 | return transformed_masks 113 | 114 | def transform_masks(self, masks, original_size, transform_matrix): 115 | """Transform masks 116 | Transform the masks back to the original image size. 117 | """ 118 | output_masks = [] 119 | for batch in range(masks.shape[0]): 120 | batch_masks = [] 121 | for mask_id in range(masks.shape[1]): 122 | mask = masks[batch, mask_id] 123 | mask = cv2.warpAffine( 124 | mask, 125 | transform_matrix[:2], 126 | (original_size[1], original_size[0]), 127 | flags=cv2.INTER_LINEAR, 128 | ) 129 | batch_masks.append(mask) 130 | output_masks.append(batch_masks) 131 | return np.array(output_masks) 132 | 133 | def encode(self, cv_image): 134 | """ 135 | Calculate embedding and metadata for a single image. 136 | """ 137 | original_size = cv_image.shape[:2] 138 | 139 | # Calculate a transformation matrix to convert to self.input_size 140 | scale_x = self.input_size[1] / cv_image.shape[1] 141 | scale_y = self.input_size[0] / cv_image.shape[0] 142 | scale = min(scale_x, scale_y) 143 | transform_matrix = np.array( 144 | [ 145 | [scale, 0, 0], 146 | [0, scale, 0], 147 | [0, 0, 1], 148 | ] 149 | ) 150 | cv_image = cv2.warpAffine( 151 | cv_image, 152 | transform_matrix[:2], 153 | (self.input_size[1], self.input_size[0]), 154 | flags=cv2.INTER_LINEAR, 155 | ) 156 | 157 | encoder_inputs = { 158 | self.encoder_input_name: cv_image.astype(np.float32), 159 | } 160 | image_embedding = self.run_encoder(encoder_inputs) 161 | return { 162 | "image_embedding": image_embedding, 163 | "original_size": original_size, 164 | "transform_matrix": transform_matrix, 165 | } 166 | 167 | def predict_masks(self, embedding, prompt): 168 | """ 169 | Predict masks for a single image. 170 | """ 171 | masks = self.run_decoder( 172 | embedding["image_embedding"], 173 | embedding["original_size"], 174 | embedding["transform_matrix"], 175 | prompt, 176 | ) 177 | 178 | return masks 179 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/segment_anything.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import traceback 4 | 5 | import cv2 6 | import onnx 7 | import numpy as np 8 | from PyQt5 import QtCore 9 | from PyQt5.QtCore import QThread 10 | from PyQt5.QtCore import QCoreApplication 11 | 12 | from anylabeling.utils import GenericWorker 13 | from anylabeling.views.labeling.shape import Shape 14 | from anylabeling.views.labeling.utils.opencv import qt_img_to_rgb_cv_img 15 | 16 | from .lru_cache import LRUCache 17 | from .model import Model 18 | from .types import AutoLabelingResult 19 | from .sam_onnx import SegmentAnythingONNX 20 | from .sam2_onnx import SegmentAnything2ONNX 21 | 22 | 23 | class SegmentAnything(Model): 24 | """Segmentation model using SegmentAnything""" 25 | 26 | class Meta: 27 | required_config_names = [ 28 | "type", 29 | "name", 30 | "display_name", 31 | "encoder_model_path", 32 | "decoder_model_path", 33 | ] 34 | widgets = [ 35 | "output_label", 36 | "output_select_combobox", 37 | "button_add_point", 38 | "button_remove_point", 39 | "button_add_rect", 40 | "button_clear", 41 | "button_finish_object", 42 | ] 43 | output_modes = { 44 | "polygon": QCoreApplication.translate("Model", "Polygon"), 45 | "rectangle": QCoreApplication.translate("Model", "Rectangle"), 46 | } 47 | default_output_mode = "polygon" 48 | 49 | def __init__(self, config_path, on_message) -> None: 50 | # Run the parent class's init method 51 | super().__init__(config_path, on_message) 52 | self.input_size = self.config["input_size"] 53 | self.max_width = self.config["max_width"] 54 | self.max_height = self.config["max_height"] 55 | 56 | # Get encoder and decoder model paths 57 | encoder_model_abs_path = self.get_model_abs_path( 58 | self.config, "encoder_model_path" 59 | ) 60 | if not encoder_model_abs_path or not os.path.isfile(encoder_model_abs_path): 61 | raise FileNotFoundError( 62 | QCoreApplication.translate( 63 | "Model", 64 | "Could not download or initialize encoder of Segment Anything.", 65 | ) 66 | ) 67 | decoder_model_abs_path = self.get_model_abs_path( 68 | self.config, "decoder_model_path" 69 | ) 70 | if not decoder_model_abs_path or not os.path.isfile(decoder_model_abs_path): 71 | raise FileNotFoundError( 72 | QCoreApplication.translate( 73 | "Model", 74 | "Could not download or initialize decoder of Segment Anything.", 75 | ) 76 | ) 77 | 78 | # Load models 79 | if self.detect_model_variant(decoder_model_abs_path) == "sam2": 80 | self.model = SegmentAnything2ONNX( 81 | encoder_model_abs_path, decoder_model_abs_path 82 | ) 83 | else: 84 | self.model = SegmentAnythingONNX( 85 | encoder_model_abs_path, decoder_model_abs_path 86 | ) 87 | 88 | # Mark for auto labeling 89 | # points, rectangles 90 | self.marks = [] 91 | 92 | # Cache for image embedding 93 | self.cache_size = 10 94 | self.preloaded_size = self.cache_size - 3 95 | self.image_embedding_cache = LRUCache(self.cache_size) 96 | 97 | # Pre-inference worker 98 | self.pre_inference_thread = None 99 | self.pre_inference_worker = None 100 | self.stop_inference = False 101 | 102 | def detect_model_variant(self, decoder_model_abs_path): 103 | """Load and detect model variant based on the model architecture""" 104 | model = onnx.load(decoder_model_abs_path) 105 | input_names = [input.name for input in model.graph.input] 106 | if "high_res_feats_0" in input_names: 107 | return "sam2" 108 | return "sam" 109 | 110 | def set_auto_labeling_marks(self, marks): 111 | """Set auto labeling marks""" 112 | self.marks = marks 113 | 114 | def post_process(self, masks): 115 | """ 116 | Post process masks 117 | """ 118 | # Find contours 119 | masks[masks > 0.0] = 255 120 | masks[masks <= 0.0] = 0 121 | masks = masks.astype(np.uint8) 122 | contours, _ = cv2.findContours(masks, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) 123 | 124 | # Refine contours 125 | approx_contours = [] 126 | for contour in contours: 127 | # Approximate contour 128 | epsilon = 0.001 * cv2.arcLength(contour, True) 129 | approx = cv2.approxPolyDP(contour, epsilon, True) 130 | approx_contours.append(approx) 131 | 132 | # Remove too big contours ( >90% of image size) 133 | if len(approx_contours) > 1: 134 | image_size = masks.shape[0] * masks.shape[1] 135 | areas = [cv2.contourArea(contour) for contour in approx_contours] 136 | filtered_approx_contours = [ 137 | contour 138 | for contour, area in zip(approx_contours, areas) 139 | if area < image_size * 0.9 140 | ] 141 | 142 | # Remove small contours (area < 20% of average area) 143 | if len(approx_contours) > 1: 144 | areas = [cv2.contourArea(contour) for contour in approx_contours] 145 | avg_area = np.mean(areas) 146 | 147 | filtered_approx_contours = [ 148 | contour 149 | for contour, area in zip(approx_contours, areas) 150 | if area > avg_area * 0.2 151 | ] 152 | approx_contours = filtered_approx_contours 153 | 154 | # Contours to shapes 155 | shapes = [] 156 | if self.output_mode == "polygon": 157 | for approx in approx_contours: 158 | # Scale points 159 | points = approx.reshape(-1, 2) 160 | points[:, 0] = points[:, 0] 161 | points[:, 1] = points[:, 1] 162 | points = points.tolist() 163 | if len(points) < 3: 164 | continue 165 | points.append(points[0]) 166 | 167 | # Create shape 168 | shape = Shape(flags={}) 169 | for point in points: 170 | point[0] = int(point[0]) 171 | point[1] = int(point[1]) 172 | shape.add_point(QtCore.QPointF(point[0], point[1])) 173 | shape.shape_type = "polygon" 174 | shape.closed = True 175 | shape.fill_color = "#000000" 176 | shape.line_color = "#000000" 177 | shape.line_width = 1 178 | shape.label = "AUTOLABEL_OBJECT" 179 | shape.selected = False 180 | shapes.append(shape) 181 | elif self.output_mode == "rectangle": 182 | x_min = 100000000 183 | y_min = 100000000 184 | x_max = 0 185 | y_max = 0 186 | for approx in approx_contours: 187 | # Scale points 188 | points = approx.reshape(-1, 2) 189 | points[:, 0] = points[:, 0] 190 | points[:, 1] = points[:, 1] 191 | points = points.tolist() 192 | if len(points) < 3: 193 | continue 194 | 195 | # Get min/max 196 | for point in points: 197 | x_min = min(x_min, point[0]) 198 | y_min = min(y_min, point[1]) 199 | x_max = max(x_max, point[0]) 200 | y_max = max(y_max, point[1]) 201 | 202 | # Create shape 203 | shape = Shape(flags={}) 204 | shape.add_point(QtCore.QPointF(x_min, y_min)) 205 | shape.add_point(QtCore.QPointF(x_max, y_max)) 206 | shape.shape_type = "rectangle" 207 | shape.closed = True 208 | shape.fill_color = "#000000" 209 | shape.line_color = "#000000" 210 | shape.line_width = 1 211 | shape.label = "AUTOLABEL_OBJECT" 212 | shape.selected = False 213 | shapes.append(shape) 214 | 215 | return shapes 216 | 217 | def predict_shapes(self, image, filename=None) -> AutoLabelingResult: 218 | """ 219 | Predict shapes from image 220 | """ 221 | if image is None or not self.marks: 222 | return AutoLabelingResult([], replace=False) 223 | 224 | shapes = [] 225 | try: 226 | # Use cached image embedding if possible 227 | cached_data = self.image_embedding_cache.get(filename) 228 | if cached_data is not None: 229 | image_embedding = cached_data 230 | else: 231 | cv_image = qt_img_to_rgb_cv_img(image, filename) 232 | if self.stop_inference: 233 | return AutoLabelingResult([], replace=False) 234 | image_embedding = self.model.encode(cv_image) 235 | self.image_embedding_cache.put( 236 | filename, 237 | image_embedding, 238 | ) 239 | if self.stop_inference: 240 | return AutoLabelingResult([], replace=False) 241 | masks = self.model.predict_masks(image_embedding, self.marks) 242 | if len(masks.shape) == 4: 243 | masks = masks[0][0] 244 | else: 245 | masks = masks[0] 246 | shapes = self.post_process(masks) 247 | except Exception as e: # noqa 248 | logging.warning("Could not inference model") 249 | logging.warning(e) 250 | traceback.print_exc() 251 | return AutoLabelingResult([], replace=False) 252 | 253 | result = AutoLabelingResult(shapes, replace=False) 254 | return result 255 | 256 | def unload(self): 257 | self.stop_inference = True 258 | if self.pre_inference_thread: 259 | self.pre_inference_thread.quit() 260 | 261 | def preload_worker(self, files): 262 | """ 263 | Preload next files, run inference and cache results 264 | """ 265 | files = files[: self.preloaded_size] 266 | for filename in files: 267 | if self.image_embedding_cache.find(filename): 268 | continue 269 | image = self.load_image_from_filename(filename) 270 | if image is None: 271 | continue 272 | if self.stop_inference: 273 | return 274 | cv_image = qt_img_to_rgb_cv_img(image) 275 | image_embedding = self.model.encode(cv_image) 276 | self.image_embedding_cache.put( 277 | filename, 278 | image_embedding, 279 | ) 280 | 281 | def on_next_files_changed(self, next_files): 282 | """ 283 | Handle next files changed. This function can preload next files 284 | and run inference to save time for user. 285 | """ 286 | if ( 287 | self.pre_inference_thread is None 288 | or not self.pre_inference_thread.isRunning() 289 | ): 290 | self.pre_inference_thread = QThread() 291 | self.pre_inference_worker = GenericWorker(self.preload_worker, next_files) 292 | self.pre_inference_worker.finished.connect(self.pre_inference_thread.quit) 293 | self.pre_inference_worker.moveToThread(self.pre_inference_thread) 294 | self.pre_inference_thread.started.connect(self.pre_inference_worker.run) 295 | self.pre_inference_thread.start() 296 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/types.py: -------------------------------------------------------------------------------- 1 | class AutoLabelingResult: 2 | def __init__(self, shapes, replace=True): 3 | """Initialize AutoLabelingResult 4 | 5 | Args: 6 | shapes (List[Shape]): List of shapes to add to the canvas. 7 | replace (bool, optional): Replaces all current shapes with 8 | new shapes. Defaults to True. 9 | """ 10 | 11 | self.shapes = shapes 12 | self.replace = replace 13 | 14 | 15 | class AutoLabelingMode: 16 | OBJECT = "AUTOLABEL_OBJECT" 17 | ADD = "AUTOLABEL_ADD" 18 | REMOVE = "AUTOLABEL_REMOVE" 19 | POINT = "point" 20 | RECTANGLE = "rectangle" 21 | 22 | def __init__(self, edit_mode, shape_type): 23 | """Initialize AutoLabelingMode 24 | 25 | Args: 26 | edit_mode (str): AUTOLABEL_ADD / AUTOLABEL_REMOVE 27 | shape_type (str): point / rectangle 28 | """ 29 | 30 | self.edit_mode = edit_mode 31 | self.shape_type = shape_type 32 | 33 | @staticmethod 34 | def get_default_mode(): 35 | """Get default mode""" 36 | return AutoLabelingMode(AutoLabelingMode.ADD, AutoLabelingMode.POINT) 37 | 38 | # Compare 2 instances of AutoLabelingMode 39 | def __eq__(self, other): 40 | if not isinstance(other, AutoLabelingMode): 41 | return False 42 | return self.edit_mode == other.edit_mode and self.shape_type == other.shape_type 43 | 44 | 45 | AutoLabelingMode.NONE = AutoLabelingMode(None, None) 46 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/yolov5.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import cv2 5 | import numpy as np 6 | from PyQt5 import QtCore 7 | from PyQt5.QtCore import QCoreApplication 8 | 9 | from anylabeling.app_info import __preferred_device__ 10 | from anylabeling.views.labeling.shape import Shape 11 | from anylabeling.views.labeling.utils.opencv import qt_img_to_rgb_cv_img 12 | from .model import Model 13 | from .types import AutoLabelingResult 14 | 15 | 16 | class YOLOv5(Model): 17 | """Object detection model using YOLOv5""" 18 | 19 | class Meta: 20 | required_config_names = [ 21 | "type", 22 | "name", 23 | "display_name", 24 | "model_path", 25 | "input_width", 26 | "input_height", 27 | "score_threshold", 28 | "nms_threshold", 29 | "confidence_threshold", 30 | "classes", 31 | ] 32 | widgets = ["button_run"] 33 | output_modes = { 34 | "rectangle": QCoreApplication.translate("Model", "Rectangle"), 35 | } 36 | default_output_mode = "rectangle" 37 | 38 | def __init__(self, model_config, on_message) -> None: 39 | # Run the parent class's init method 40 | super().__init__(model_config, on_message) 41 | 42 | model_abs_path = self.get_model_abs_path(self.config, "model_path") 43 | if not model_abs_path or not os.path.isfile(model_abs_path): 44 | raise FileNotFoundError( 45 | QCoreApplication.translate( 46 | "Model", "Could not download or initialize YOLOv5 model." 47 | ) 48 | ) 49 | 50 | self.net = cv2.dnn.readNet(model_abs_path) 51 | if __preferred_device__ == "GPU": 52 | self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) 53 | self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) 54 | self.classes = self.config["classes"] 55 | 56 | def pre_process(self, input_image, net): 57 | """ 58 | Pre-process the input image before feeding it to the network. 59 | """ 60 | # Create a 4D blob from a frame. 61 | blob = cv2.dnn.blobFromImage( 62 | input_image, 63 | 1 / 255, 64 | (self.config["input_width"], self.config["input_height"]), 65 | [0, 0, 0], 66 | 1, 67 | crop=False, 68 | ) 69 | 70 | # Sets the input to the network. 71 | net.setInput(blob) 72 | 73 | # Runs the forward pass to get output of the output layers. 74 | output_layers = net.getUnconnectedOutLayersNames() 75 | outputs = net.forward(output_layers) 76 | 77 | return outputs 78 | 79 | def post_process(self, input_image, outputs): 80 | """ 81 | Post-process the network's output, to get the bounding boxes and 82 | their confidence scores. 83 | """ 84 | # Lists to hold respective values while unwrapping. 85 | class_ids = [] 86 | confidences = [] 87 | boxes = [] 88 | 89 | # Rows. 90 | rows = outputs[0].shape[1] 91 | 92 | image_height, image_width = input_image.shape[:2] 93 | 94 | # Resizing factor. 95 | x_factor = image_width / self.config["input_width"] 96 | y_factor = image_height / self.config["input_height"] 97 | 98 | # Iterate through 25200 detections. 99 | for r in range(rows): 100 | row = outputs[0][0][r] 101 | confidence = row[4] 102 | 103 | # Discard bad detections and continue. 104 | if confidence >= self.config["confidence_threshold"]: 105 | classes_scores = row[5:] 106 | 107 | # Get the index of max class score. 108 | class_id = np.argmax(classes_scores) 109 | 110 | # Continue if the class score is above threshold. 111 | if classes_scores[class_id] > self.config["score_threshold"]: 112 | confidences.append(confidence) 113 | class_ids.append(class_id) 114 | 115 | cx, cy, w, h = row[0], row[1], row[2], row[3] 116 | 117 | left = int((cx - w / 2) * x_factor) 118 | top = int((cy - h / 2) * y_factor) 119 | width = int(w * x_factor) 120 | height = int(h * y_factor) 121 | 122 | box = np.array([left, top, width, height]) 123 | boxes.append(box) 124 | 125 | # Perform non maximum suppression to eliminate redundant 126 | # overlapping boxes with lower confidences. 127 | indices = cv2.dnn.NMSBoxes( 128 | boxes, 129 | confidences, 130 | self.config["confidence_threshold"], 131 | self.config["nms_threshold"], 132 | ) 133 | 134 | output_boxes = [] 135 | for i in indices: 136 | box = boxes[i] 137 | left = box[0] 138 | top = box[1] 139 | width = box[2] 140 | height = box[3] 141 | label = self.classes[class_ids[i]] 142 | score = confidences[i] 143 | 144 | output_box = { 145 | "x1": left, 146 | "y1": top, 147 | "x2": left + width, 148 | "y2": top + height, 149 | "label": label, 150 | "score": score, 151 | } 152 | 153 | output_boxes.append(output_box) 154 | 155 | return output_boxes 156 | 157 | def predict_shapes(self, image, image_path=None): 158 | """ 159 | Predict shapes from image 160 | """ 161 | 162 | if image is None: 163 | return [] 164 | 165 | try: 166 | image = qt_img_to_rgb_cv_img(image, image_path) 167 | except Exception as e: # noqa 168 | logging.warning("Could not inference model") 169 | logging.warning(e) 170 | return [] 171 | 172 | detections = self.pre_process(image, self.net) 173 | boxes = self.post_process(image, detections) 174 | shapes = [] 175 | 176 | for box in boxes: 177 | shape = Shape(label=box["label"], shape_type="rectangle", flags={}) 178 | shape.add_point(QtCore.QPointF(box["x1"], box["y1"])) 179 | shape.add_point(QtCore.QPointF(box["x2"], box["y2"])) 180 | shapes.append(shape) 181 | 182 | result = AutoLabelingResult(shapes, replace=True) 183 | return result 184 | 185 | def unload(self): 186 | del self.net 187 | -------------------------------------------------------------------------------- /anylabeling/services/auto_labeling/yolov8.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import cv2 5 | import numpy as np 6 | from PyQt5 import QtCore 7 | from PyQt5.QtCore import QCoreApplication 8 | 9 | from anylabeling.app_info import __preferred_device__ 10 | from anylabeling.views.labeling.shape import Shape 11 | from anylabeling.views.labeling.utils.opencv import qt_img_to_rgb_cv_img 12 | from .model import Model 13 | from .types import AutoLabelingResult 14 | 15 | 16 | class YOLOv8(Model): 17 | """Object detection model using YOLOv8""" 18 | 19 | class Meta: 20 | required_config_names = [ 21 | "type", 22 | "name", 23 | "display_name", 24 | "model_path", 25 | "input_width", 26 | "input_height", 27 | "score_threshold", 28 | "nms_threshold", 29 | "confidence_threshold", 30 | "classes", 31 | ] 32 | widgets = ["button_run"] 33 | output_modes = { 34 | "rectangle": QCoreApplication.translate("Model", "Rectangle"), 35 | } 36 | default_output_mode = "rectangle" 37 | 38 | def __init__(self, model_config, on_message) -> None: 39 | # Run the parent class's init method 40 | super().__init__(model_config, on_message) 41 | 42 | model_abs_path = self.get_model_abs_path(self.config, "model_path") 43 | if not model_abs_path or not os.path.isfile(model_abs_path): 44 | raise FileNotFoundError( 45 | QCoreApplication.translate( 46 | "Model", "Could not download or initialize YOLOv8 model." 47 | ) 48 | ) 49 | 50 | self.net = cv2.dnn.readNet(model_abs_path) 51 | if __preferred_device__ == "GPU": 52 | self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) 53 | self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) 54 | self.classes = self.config["classes"] 55 | 56 | def pre_process(self, input_image, net): 57 | """ 58 | Pre-process the input image before feeding it to the network. 59 | """ 60 | # Create a 4D blob from a frame. 61 | blob = cv2.dnn.blobFromImage( 62 | input_image, 63 | 1 / 255, 64 | (self.config["input_width"], self.config["input_height"]), 65 | [0, 0, 0], 66 | 1, 67 | crop=False, 68 | ) 69 | 70 | # Sets the input to the network. 71 | net.setInput(blob) 72 | 73 | # Runs the forward with blob. 74 | outputs = net.forward() 75 | outputs = np.array([cv2.transpose(outputs[0])]) 76 | 77 | return outputs 78 | 79 | def post_process(self, input_image, outputs): 80 | """ 81 | Post-process the network's output, to get the bounding boxes and 82 | their confidence scores. 83 | """ 84 | # Lists to hold respective values while unwrapping. 85 | class_ids = [] 86 | confidences = [] 87 | boxes = [] 88 | 89 | # Rows. 90 | rows = outputs.shape[1] 91 | 92 | image_height, image_width = input_image.shape[:2] 93 | 94 | # Resizing factor. 95 | x_factor = image_width / self.config["input_width"] 96 | y_factor = image_height / self.config["input_height"] 97 | 98 | # Iterate through 8400 rows. 99 | for r in range(rows): 100 | row = outputs[0][r] 101 | classes_scores = row[4:] 102 | 103 | # Get the index of max class score and confidence. 104 | _, confidence, _, (_, class_id) = cv2.minMaxLoc(classes_scores) 105 | 106 | # Discard confidence lower than threshold 107 | if confidence >= self.config["confidence_threshold"]: 108 | confidences.append(confidence) 109 | class_ids.append(class_id) 110 | 111 | cx, cy, w, h = row[0], row[1], row[2], row[3] 112 | 113 | left = int((cx - w / 2) * x_factor) 114 | top = int((cy - h / 2) * y_factor) 115 | width = int(w * x_factor) 116 | height = int(h * y_factor) 117 | 118 | box = np.array([left, top, width, height]) 119 | boxes.append(box) 120 | 121 | # Perform non maximum suppression to eliminate redundant 122 | # overlapping boxes with lower confidences. 123 | indices = cv2.dnn.NMSBoxes( 124 | boxes, 125 | confidences, 126 | self.config["confidence_threshold"], 127 | self.config["nms_threshold"], 128 | ) 129 | 130 | output_boxes = [] 131 | for i in indices: 132 | box = boxes[i] 133 | left = box[0] 134 | top = box[1] 135 | width = box[2] 136 | height = box[3] 137 | label = self.classes[class_ids[i]] 138 | score = confidences[i] 139 | 140 | output_box = { 141 | "x1": left, 142 | "y1": top, 143 | "x2": left + width, 144 | "y2": top + height, 145 | "label": label, 146 | "score": score, 147 | } 148 | 149 | output_boxes.append(output_box) 150 | 151 | return output_boxes 152 | 153 | def predict_shapes(self, image, image_path=None): 154 | """ 155 | Predict shapes from image 156 | """ 157 | 158 | if image is None: 159 | return [] 160 | 161 | try: 162 | image = qt_img_to_rgb_cv_img(image, image_path) 163 | except Exception as e: # noqa 164 | logging.warning("Could not inference model") 165 | logging.warning(e) 166 | return [] 167 | 168 | detections = self.pre_process(image, self.net) 169 | boxes = self.post_process(image, detections) 170 | shapes = [] 171 | 172 | for box in boxes: 173 | shape = Shape(label=box["label"], shape_type="rectangle", flags={}) 174 | shape.add_point(QtCore.QPointF(box["x1"], box["y1"])) 175 | shape.add_point(QtCore.QPointF(box["x2"], box["y2"])) 176 | shapes.append(shape) 177 | 178 | result = AutoLabelingResult(shapes, replace=True) 179 | return result 180 | 181 | def unload(self): 182 | del self.net 183 | -------------------------------------------------------------------------------- /anylabeling/styles/__init__.py: -------------------------------------------------------------------------------- 1 | from .theme import AppTheme 2 | 3 | __all__ = ["AppTheme"] 4 | -------------------------------------------------------------------------------- /anylabeling/styles/theme.py: -------------------------------------------------------------------------------- 1 | import darkdetect 2 | from PyQt5.QtGui import QPalette, QColor 3 | import os 4 | 5 | 6 | class AppTheme: 7 | """ 8 | Theme manager for the application 9 | Provides consistent styling for both light and dark themes 10 | """ 11 | 12 | # Modern color palette 13 | PRIMARY_LIGHT = "#2196F3" # Blue 14 | PRIMARY_DARK = "#1976D2" # Darker Blue 15 | ACCENT_LIGHT = "#FFA000" # Amber 16 | ACCENT_DARK = "#FF8F00" # Darker Amber 17 | 18 | # Light theme colors 19 | LIGHT = { 20 | "window": "#FFFFFF", 21 | "window_text": "#212121", 22 | "base": "#F5F5F5", 23 | "alternate_base": "#E0E0E0", 24 | "text": "#212121", 25 | "button": "#E0E0E0", 26 | "button_text": "#212121", 27 | "bright_text": "#000000", 28 | "highlight": PRIMARY_LIGHT, 29 | "highlighted_text": "#FFFFFF", 30 | "link": PRIMARY_LIGHT, 31 | "dark": "#455A64", 32 | "mid": "#9E9E9E", 33 | "midlight": "#BDBDBD", 34 | "light": "#F5F5F5", 35 | # Custom colors 36 | "border": "#BDBDBD", 37 | "toolbar_bg": "#FFFFFF", 38 | "dock_title_bg": "#E0E0E0", 39 | "dock_title_text": "#212121", 40 | "success": "#4CAF50", 41 | "warning": "#FFC107", 42 | "error": "#F44336", 43 | "panel_bg": "#FFFFFF", 44 | "selection": "#BBDEFB", 45 | } 46 | 47 | # Dark theme colors 48 | DARK = { 49 | "window": "#212121", 50 | "window_text": "#EEEEEE", 51 | "base": "#303030", 52 | "alternate_base": "#424242", 53 | "text": "#EEEEEE", 54 | "button": "#424242", 55 | "button_text": "#EEEEEE", 56 | "bright_text": "#FFFFFF", 57 | "highlight": PRIMARY_DARK, 58 | "highlighted_text": "#FFFFFF", 59 | "link": PRIMARY_LIGHT, 60 | "dark": "#2D2D2D", 61 | "mid": "#616161", 62 | "midlight": "#757575", 63 | "light": "#424242", 64 | # Custom colors 65 | "border": "#616161", 66 | "toolbar_bg": "#333333", 67 | "dock_title_bg": "#424242", 68 | "dock_title_text": "#EEEEEE", 69 | "success": "#4CAF50", 70 | "warning": "#FFC107", 71 | "error": "#F44336", 72 | "panel_bg": "#303030", 73 | "selection": "#0D47A1", 74 | } 75 | 76 | @staticmethod 77 | def is_dark_mode(): 78 | """Check if system is using dark mode or if it's set via environment variable""" 79 | # Check environment variable first 80 | if "DARK_MODE" in os.environ: 81 | return os.environ["DARK_MODE"] == "1" 82 | # Fall back to system detection 83 | return darkdetect.isDark() 84 | 85 | @staticmethod 86 | def get_color(color_name): 87 | """Get color based on current theme""" 88 | is_dark = AppTheme.is_dark_mode() 89 | colors = AppTheme.DARK if is_dark else AppTheme.LIGHT 90 | return colors.get(color_name, "#FFFFFF" if not is_dark else "#212121") 91 | 92 | @staticmethod 93 | def apply_theme(app): 94 | """Apply theme to entire application""" 95 | is_dark = AppTheme.is_dark_mode() 96 | colors = AppTheme.DARK if is_dark else AppTheme.LIGHT 97 | 98 | # Set application style 99 | app.setStyle("Fusion") 100 | 101 | # Create and apply palette 102 | palette = QPalette() 103 | palette.setColor(QPalette.Window, QColor(colors["window"])) 104 | palette.setColor(QPalette.WindowText, QColor(colors["window_text"])) 105 | palette.setColor(QPalette.Base, QColor(colors["base"])) 106 | palette.setColor(QPalette.AlternateBase, QColor(colors["alternate_base"])) 107 | palette.setColor(QPalette.Text, QColor(colors["text"])) 108 | palette.setColor(QPalette.Button, QColor(colors["button"])) 109 | palette.setColor(QPalette.ButtonText, QColor(colors["button_text"])) 110 | palette.setColor(QPalette.BrightText, QColor(colors["bright_text"])) 111 | palette.setColor(QPalette.Highlight, QColor(colors["highlight"])) 112 | palette.setColor(QPalette.HighlightedText, QColor(colors["highlighted_text"])) 113 | palette.setColor(QPalette.Link, QColor(colors["link"])) 114 | palette.setColor(QPalette.Dark, QColor(colors["dark"])) 115 | palette.setColor(QPalette.Mid, QColor(colors["mid"])) 116 | palette.setColor(QPalette.Midlight, QColor(colors["midlight"])) 117 | palette.setColor(QPalette.Light, QColor(colors["light"])) 118 | 119 | app.setPalette(palette) 120 | 121 | # Apply global stylesheet 122 | app.setStyleSheet(AppTheme.get_stylesheet()) 123 | 124 | @staticmethod 125 | def get_stylesheet(): 126 | """Get stylesheet for current theme""" 127 | is_dark = AppTheme.is_dark_mode() 128 | colors = AppTheme.DARK if is_dark else AppTheme.LIGHT 129 | 130 | return f""" 131 | /* Main Window */ 132 | QMainWindow {{ 133 | background-color: {colors["window"]}; 134 | color: {colors["window_text"]}; 135 | }} 136 | 137 | /* Menus and Menu Bar */ 138 | QMenuBar {{ 139 | background-color: {colors["window"]}; 140 | color: {colors["window_text"]}; 141 | border-bottom: 1px solid {colors["border"]}; 142 | }} 143 | 144 | QMenuBar::item {{ 145 | background-color: transparent; 146 | padding: 4px 10px; 147 | }} 148 | 149 | QMenuBar::item:selected {{ 150 | background-color: {colors["highlight"]}; 151 | color: {colors["highlighted_text"]}; 152 | }} 153 | 154 | QDockWidget::title {{ 155 | text-align: center; 156 | border-radius: 4px; 157 | margin-bottom: 2px; 158 | background-color: {colors["dock_title_bg"]}; 159 | color: {colors["dock_title_text"]}; 160 | }} 161 | 162 | /* Tool Bar */ 163 | QToolBar {{ 164 | background-color: {colors["toolbar_bg"]}; 165 | padding: 2px; 166 | border: none; 167 | border-bottom: 1px solid {colors["border"]}; 168 | }} 169 | 170 | QToolButton {{ 171 | background-color: transparent; 172 | border: 1px solid transparent; 173 | border-radius: 4px; 174 | padding: 4px; 175 | margin: 1px; 176 | }} 177 | 178 | QToolButton:hover {{ 179 | background-color: {colors["alternate_base"]}; 180 | border: 1px solid {colors["border"]}; 181 | }} 182 | 183 | QToolButton:pressed {{ 184 | background-color: {colors["midlight"]}; 185 | }} 186 | 187 | QToolButton:checked {{ 188 | background-color: {colors["highlight"]}; 189 | color: {colors["highlighted_text"]}; 190 | }} 191 | 192 | /* List Widgets */ 193 | QListWidget {{ 194 | background-color: {colors["base"]}; 195 | color: {colors["text"]}; 196 | border: 1px solid {colors["border"]}; 197 | border-radius: 4px; 198 | }} 199 | 200 | QListWidget::item:selected {{ 201 | background-color: {colors["highlight"]}; 202 | color: {colors["highlighted_text"]}; 203 | }} 204 | 205 | QListWidget::item:hover:!selected {{ 206 | background-color: {colors["selection"]}; 207 | }} 208 | 209 | /* Scroll Areas and Scroll Bars */ 210 | QScrollArea {{ 211 | background-color: {colors["window"]}; 212 | border: none; 213 | }} 214 | 215 | QScrollBar:vertical {{ 216 | background-color: {colors["base"]}; 217 | width: 12px; 218 | margin: 0px; 219 | }} 220 | 221 | QScrollBar::handle:vertical {{ 222 | background-color: {colors["mid"]}; 223 | min-height: 20px; 224 | border-radius: 6px; 225 | }} 226 | 227 | QScrollBar::handle:vertical:hover {{ 228 | background-color: {colors["highlight"]}; 229 | }} 230 | 231 | QScrollBar:horizontal {{ 232 | background-color: {colors["base"]}; 233 | height: 12px; 234 | margin: 0px; 235 | }} 236 | 237 | QScrollBar::handle:horizontal {{ 238 | background-color: {colors["mid"]}; 239 | min-width: 20px; 240 | border-radius: 6px; 241 | }} 242 | 243 | QScrollBar::handle:horizontal:hover {{ 244 | background-color: {colors["highlight"]}; 245 | }} 246 | 247 | QScrollBar::add-line, QScrollBar::sub-line {{ 248 | width: 0px; 249 | height: 0px; 250 | }} 251 | 252 | /* Tab Widget */ 253 | QTabWidget::pane {{ 254 | border: 1px solid {colors["border"]}; 255 | border-radius: 4px; 256 | top: -1px; 257 | }} 258 | 259 | QTabBar::tab {{ 260 | background-color: {colors["alternate_base"]}; 261 | color: {colors["text"]}; 262 | border: 1px solid {colors["border"]}; 263 | border-bottom-color: {colors["border"]}; 264 | border-top-left-radius: 4px; 265 | border-top-right-radius: 4px; 266 | padding: 6px 12px; 267 | min-width: 80px; 268 | min-height: 20px; 269 | }} 270 | 271 | QTabBar::tab:selected {{ 272 | background-color: {colors["window"]}; 273 | border-bottom-color: {colors["window"]}; 274 | }} 275 | 276 | QTabBar::tab:!selected {{ 277 | margin-top: 2px; 278 | }} 279 | 280 | /* Progress Bar */ 281 | QProgressBar {{ 282 | background-color: {colors["base"]}; 283 | color: {colors["highlighted_text"]}; 284 | border: 1px solid {colors["border"]}; 285 | border-radius: 4px; 286 | text-align: center; 287 | }} 288 | 289 | QProgressBar::chunk {{ 290 | background-color: {colors["highlight"]}; 291 | width: 10px; 292 | margin: 0.5px; 293 | }} 294 | 295 | /* Status Bar */ 296 | QStatusBar {{ 297 | background-color: {colors["window"]}; 298 | color: {colors["window_text"]}; 299 | border-top: 1px solid {colors["border"]}; 300 | }} 301 | 302 | QStatusBar::item {{ 303 | border: none; 304 | }} 305 | 306 | /* Specific Widget Styling */ 307 | #zoomWidget QToolButton {{ 308 | margin: 0px 1px; 309 | padding: 2px; 310 | }} 311 | 312 | /* Auto Labeling Widget */ 313 | #autoLabelingWidget {{ 314 | background-color: {colors["panel_bg"]}; 315 | border: 1px solid {colors["border"]}; 316 | border-radius: 4px; 317 | }} 318 | 319 | #autoLabelingWidget QPushButton {{ 320 | min-height: 24px; 321 | }} 322 | """ 323 | -------------------------------------------------------------------------------- /anylabeling/utils.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot 2 | 3 | 4 | class GenericWorker(QObject): 5 | finished = pyqtSignal() 6 | 7 | def __init__(self, func, *args, **kwargs): 8 | super().__init__() 9 | self.func = func 10 | self.args = args 11 | self.kwargs = kwargs 12 | 13 | @pyqtSlot() 14 | def run(self): 15 | self.func(*self.args, **self.kwargs) 16 | self.finished.emit() 17 | -------------------------------------------------------------------------------- /anylabeling/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/views/__init__.py -------------------------------------------------------------------------------- /anylabeling/views/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/views/common/__init__.py -------------------------------------------------------------------------------- /anylabeling/views/common/toaster.py: -------------------------------------------------------------------------------- 1 | """Defines Toaster widget""" 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | 5 | 6 | class QToaster(QtWidgets.QFrame): 7 | """Toaster widget 8 | For displaying a short notification which can be hide after a duration 9 | """ 10 | 11 | closed = QtCore.pyqtSignal() 12 | 13 | def __init__(self, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | QtWidgets.QHBoxLayout(self) 16 | 17 | self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 18 | 19 | self.setStyleSheet( 20 | """ 21 | QToaster { 22 | border: 1px solid black; 23 | border-radius: 0px; 24 | color: rgb(30, 30, 30); 25 | background-color: rgb(255, 255, 255); 26 | } 27 | """ 28 | ) 29 | # alternatively: 30 | self.setAutoFillBackground(True) 31 | self.setFrameShape(self.Box) 32 | 33 | self.timer = QtCore.QTimer(singleShot=True, timeout=self.hide) 34 | 35 | if self.parent(): 36 | self.opacity_effect = QtWidgets.QGraphicsOpacityEffect(opacity=0) 37 | self.setGraphicsEffect(self.opacity_effect) 38 | self.opacity_ani = QtCore.QPropertyAnimation( 39 | self.opacity_effect, b"opacity" 40 | ) 41 | # we have a parent, install an eventFilter so that when it's resized 42 | # the notification will be correctly moved to the right corner 43 | self.parent().installEventFilter(self) 44 | else: 45 | # there's no parent, use the window opacity property, assuming that 46 | # the window manager supports it; if it doesn't, this won'd do 47 | # anything (besides making the hiding a bit longer by half a 48 | # second) 49 | self.opacity_ani = QtCore.QPropertyAnimation(self, b"windowOpacity") 50 | self.opacity_ani.setStartValue(0.0) 51 | self.opacity_ani.setEndValue(1.0) 52 | self.opacity_ani.setDuration(100) 53 | self.opacity_ani.finished.connect(self.check_closed) 54 | 55 | self.corner = QtCore.Qt.TopLeftCorner 56 | self.margin = 10 57 | 58 | def check_closed(self): 59 | """Close the toaster after fading out""" 60 | # if we have been fading out, we're closing the notification 61 | if self.opacity_ani.direction() == self.opacity_ani.Backward: 62 | self.close() 63 | 64 | def restore(self): 65 | """Restore toaster (timer + opacity)""" 66 | # this is a "helper function", that can be called from mouseEnterEvent 67 | # and when the parent widget is resized. We will not close the 68 | # notification if the mouse is in or the parent is resized 69 | self.timer.stop() 70 | # also, stop the animation if it's fading out... 71 | self.opacity_ani.stop() 72 | # ...and restore the opacity 73 | if self.parent(): 74 | self.opacity_effect.setOpacity(1) 75 | else: 76 | self.setWindowOpacity(1) 77 | 78 | def hide(self): 79 | """Hide toaster by opacity effect""" 80 | self.opacity_ani.setDirection(self.opacity_ani.Backward) 81 | self.opacity_ani.setDuration(500) 82 | self.opacity_ani.start() 83 | 84 | def eventFilter(self, source, event): 85 | """Event filter""" 86 | if source == self.parent() and event.type() == QtCore.QEvent.Resize: 87 | self.opacity_ani.stop() 88 | parent_rect = self.parent().rect() 89 | geo = self.geometry() 90 | margin = int(self.margin) 91 | if self.corner == QtCore.Qt.TopLeftCorner: 92 | geo.moveTopLeft(parent_rect.topLeft() + QtCore.QPoint(margin, margin)) 93 | elif self.corner == QtCore.Qt.TopRightCorner: 94 | geo.moveTopRight( 95 | parent_rect.topRight() + QtCore.QPoint(-margin, margin) 96 | ) 97 | elif self.corner == QtCore.Qt.BottomRightCorner: 98 | geo.moveBottomRight( 99 | parent_rect.bottomRight() + QtCore.QPoint(-margin, -margin) 100 | ) 101 | else: 102 | geo.moveBottomLeft( 103 | parent_rect.bottomLeft() + QtCore.QPoint(margin, -margin) 104 | ) 105 | self.setGeometry(geo) 106 | self.restore() 107 | self.timer.start() 108 | return super().eventFilter(source, event) 109 | 110 | def enterEvent(self, _): 111 | """ 112 | Restore toaster (opacity) when move mouse into it 113 | Keep it open as long as the mouse does not leave 114 | """ 115 | self.restore() 116 | 117 | def leaveEvent(self, _): 118 | """ 119 | When mouse leaves the toaster, start the timer again to 120 | count down to close event 121 | """ 122 | self.timer.start() 123 | 124 | def closeEvent(self, _): 125 | """Handle close event""" 126 | # we don't need the notification anymore, delete it! 127 | self.deleteLater() 128 | 129 | def resizeEvent(self, event): 130 | """Handles request event""" 131 | super().resizeEvent(event) 132 | # if you don't set a stylesheet, you don't need any of the following! 133 | if not self.parent(): 134 | # there's no parent, so we need to update the mask 135 | path = QtGui.QPainterPath() 136 | path.addRoundedRect(QtCore.QRectF(self.rect()).translated(-0.5, -0.5), 4, 4) 137 | self.setMask( 138 | QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()) 139 | ) 140 | else: 141 | self.clearMask() 142 | 143 | @staticmethod 144 | def show_message( 145 | parent, 146 | message, 147 | corner=QtCore.Qt.TopLeftCorner, 148 | margin=10, 149 | closable=True, 150 | timeout=5000, 151 | desktop=False, 152 | parent_window=True, 153 | ): # pylint: disable=too-many-statements,too-many-locals,too-many-arguments 154 | """Show message as a toaster""" 155 | margin = int(margin) 156 | 157 | if parent and parent_window: 158 | parent = parent.window() 159 | 160 | if not parent or desktop: 161 | self = QToaster(None) 162 | self.setWindowFlags( 163 | self.windowFlags() 164 | | QtCore.Qt.FramelessWindowHint 165 | | QtCore.Qt.BypassWindowManagerHint 166 | ) 167 | # This is a dirty hack! 168 | # parentless objects are garbage collected, so the widget will be 169 | # deleted as soon as the function that calls it returns, but if an 170 | # object is referenced to *any* other object it will not, at least 171 | # for PyQt (I didn't test it to a deeper level) 172 | self.__self = self 173 | 174 | current_screen = QtWidgets.QApplication.primaryScreen() 175 | if parent and parent.window().geometry().size().isValid(): 176 | # the notification is to be shown on the desktop, but there is a 177 | # parent that is (theoretically) visible and mapped, we'll try to 178 | # use its geometry as a reference to guess which desktop shows 179 | # most of its area; if the parent is not a top level window, use 180 | # that as a reference 181 | reference = parent.window().geometry() 182 | else: 183 | # the parent has not been mapped yet, let's use the cursor as a 184 | # reference for the screen 185 | reference = QtCore.QRect( 186 | QtGui.QCursor.pos() - QtCore.QPoint(1, 1), 187 | QtCore.QSize(3, 3), 188 | ) 189 | max_area = 0 190 | for screen in QtWidgets.QApplication.screens(): 191 | intersected = screen.geometry().intersected(reference) 192 | area = intersected.width() * intersected.height() 193 | if area > max_area: 194 | max_area = area 195 | current_screen = screen 196 | parent_rect = current_screen.availableGeometry() 197 | else: 198 | self = QToaster(parent) 199 | parent_rect = parent.rect() 200 | 201 | self.timer.setInterval(timeout) 202 | 203 | label = QtWidgets.QLabel(message) 204 | label.setStyleSheet("color: rgb(33, 33, 33);") 205 | font = QtGui.QFont() 206 | font.setPointSize(10) 207 | font.setWeight(100) 208 | label.setFont(font) 209 | self.layout().addWidget(label) 210 | 211 | if closable: 212 | close_button = QtWidgets.QToolButton() 213 | self.layout().addWidget(close_button) 214 | close_icon = self.style().standardIcon( 215 | QtWidgets.QStyle.SP_TitleBarCloseButton 216 | ) 217 | close_button.setIcon(close_icon) 218 | close_button.setAutoRaise(True) 219 | close_button.clicked.connect(self.close) 220 | 221 | self.timer.start() 222 | 223 | # raise the widget and adjust its size to the minimum 224 | self.raise_() 225 | self.adjustSize() 226 | 227 | self.corner = corner 228 | self.margin = margin 229 | 230 | geo = self.geometry() 231 | # now the widget should have the correct size hints, let's move it to the 232 | # right place 233 | if corner == QtCore.Qt.TopLeftCorner: 234 | geo.moveTopLeft(parent_rect.topLeft() + QtCore.QPoint(margin, margin)) 235 | elif corner == QtCore.Qt.TopRightCorner: 236 | geo.moveTopRight(parent_rect.topRight() + QtCore.QPoint(-margin, margin)) 237 | elif corner == QtCore.Qt.BottomRightCorner: 238 | geo.moveBottomRight( 239 | parent_rect.bottomRight() + QtCore.QPoint(-margin, -margin) 240 | ) 241 | else: 242 | geo.moveBottomLeft( 243 | parent_rect.bottomLeft() + QtCore.QPoint(margin, -margin) 244 | ) 245 | 246 | self.setGeometry(geo) 247 | self.show() 248 | self.opacity_ani.start() 249 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/anylabeling/views/labeling/__init__.py -------------------------------------------------------------------------------- /anylabeling/views/labeling/label_file.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import contextlib 3 | import io 4 | import json 5 | import os.path as osp 6 | 7 | import PIL.Image 8 | 9 | from ...app_info import __version__ 10 | from . import utils 11 | from .logger import logger 12 | 13 | PIL.Image.MAX_IMAGE_PIXELS = None 14 | 15 | 16 | @contextlib.contextmanager 17 | def io_open(name, mode): 18 | assert mode in ["r", "w"] 19 | encoding = "utf-8" 20 | yield io.open(name, mode, encoding=encoding) 21 | 22 | 23 | class LabelFileError(Exception): 24 | pass 25 | 26 | 27 | class LabelFile: 28 | suffix = ".json" 29 | 30 | def __init__(self, filename=None): 31 | self.shapes = [] 32 | self.image_path = None 33 | self.image_data = None 34 | if filename is not None: 35 | self.load(filename) 36 | self.filename = filename 37 | 38 | @staticmethod 39 | def load_image_file(filename): 40 | try: 41 | image_pil = PIL.Image.open(filename) 42 | except IOError: 43 | logger.error("Failed opening image file: %s", filename) 44 | return None 45 | 46 | # apply orientation to image according to exif 47 | image_pil = utils.apply_exif_orientation(image_pil) 48 | 49 | with io.BytesIO() as f: 50 | ext = osp.splitext(filename)[1].lower() 51 | if ext in [".jpg", ".jpeg"]: 52 | image_pil = image_pil.convert("RGB") 53 | img_format = "JPEG" 54 | else: 55 | img_format = "PNG" 56 | image_pil.save(f, format=img_format) 57 | f.seek(0) 58 | return f.read() 59 | 60 | def load(self, filename): 61 | keys = [ 62 | "version", 63 | "imageData", 64 | "imagePath", 65 | "shapes", # polygonal annotations 66 | "flags", # image level flags 67 | "imageHeight", 68 | "imageWidth", 69 | ] 70 | shape_keys = [ 71 | "label", 72 | "text", 73 | "points", 74 | "group_id", 75 | "shape_type", 76 | "flags", 77 | ] 78 | try: 79 | with io_open(filename, "r") as f: 80 | data = json.load(f) 81 | version = data.get("version") 82 | if version is None: 83 | logger.warning("Loading JSON file (%s) of unknown version", filename) 84 | 85 | if data["imageData"] is not None: 86 | image_data = base64.b64decode(data["imageData"]) 87 | else: 88 | # relative path from label file to relative path from cwd 89 | image_path = osp.join(osp.dirname(filename), data["imagePath"]) 90 | image_data = self.load_image_file(image_path) 91 | flags = data.get("flags") or {} 92 | image_path = data["imagePath"] 93 | self._check_image_height_and_width( 94 | base64.b64encode(image_data).decode("utf-8"), 95 | data.get("imageHeight"), 96 | data.get("imageWidth"), 97 | ) 98 | shapes = [ 99 | { 100 | "label": s["label"], 101 | "text": s.get("text", ""), 102 | "points": s["points"], 103 | "shape_type": s.get("shape_type", "polygon"), 104 | "flags": s.get("flags", {}), 105 | "group_id": s.get("group_id"), 106 | "other_data": {k: v for k, v in s.items() if k not in shape_keys}, 107 | } 108 | for s in data["shapes"] 109 | ] 110 | except Exception as e: # noqa 111 | raise LabelFileError(e) from e 112 | 113 | other_data = {} 114 | for key, value in data.items(): 115 | if key not in keys: 116 | other_data[key] = value 117 | 118 | # Add new fields if not available 119 | other_data["text"] = other_data.get("text", "") 120 | 121 | # Only replace data after everything is loaded. 122 | self.flags = flags 123 | self.shapes = shapes 124 | self.image_path = image_path 125 | self.image_data = image_data 126 | self.filename = filename 127 | self.other_data = other_data 128 | 129 | @staticmethod 130 | def _check_image_height_and_width(image_data, image_height, image_width): 131 | img_arr = utils.img_b64_to_arr(image_data) 132 | if image_height is not None and img_arr.shape[0] != image_height: 133 | logger.error( 134 | "image_height does not match with image_data or image_path, " 135 | "so getting image_height from actual image." 136 | ) 137 | image_height = img_arr.shape[0] 138 | if image_width is not None and img_arr.shape[1] != image_width: 139 | logger.error( 140 | "image_width does not match with image_data or image_path, " 141 | "so getting image_width from actual image." 142 | ) 143 | image_width = img_arr.shape[1] 144 | return image_height, image_width 145 | 146 | def save( 147 | self, 148 | filename=None, 149 | shapes=None, 150 | image_path=None, 151 | image_height=None, 152 | image_width=None, 153 | image_data=None, 154 | other_data=None, 155 | flags=None, 156 | ): 157 | if image_data is not None: 158 | image_data = base64.b64encode(image_data).decode("utf-8") 159 | image_height, image_width = self._check_image_height_and_width( 160 | image_data, image_height, image_width 161 | ) 162 | if other_data is None: 163 | other_data = {} 164 | if flags is None: 165 | flags = {} 166 | data = { 167 | "version": __version__, 168 | "flags": flags, 169 | "shapes": shapes, 170 | "imagePath": image_path, 171 | "imageData": image_data, 172 | "imageHeight": image_height, 173 | "imageWidth": image_width, 174 | } 175 | for key, value in other_data.items(): 176 | assert key not in data 177 | data[key] = value 178 | try: 179 | with io_open(filename, "w") as f: 180 | json.dump(data, f, ensure_ascii=False, indent=2) 181 | self.filename = filename 182 | except Exception as e: # noqa 183 | raise LabelFileError(e) from e 184 | 185 | @staticmethod 186 | def is_label_file(filename): 187 | return osp.splitext(filename)[1].lower() == LabelFile.suffix 188 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/label_wrapper.py: -------------------------------------------------------------------------------- 1 | """This module defines labeling wrapper and related functions""" 2 | 3 | from PyQt5.QtWidgets import QVBoxLayout, QWidget 4 | 5 | from .label_widget import LabelingWidget 6 | 7 | 8 | class LabelingWrapper(QWidget): 9 | """Wrapper widget for labeling module""" 10 | 11 | def __init__( 12 | self, 13 | parent, 14 | config=None, 15 | filename=None, 16 | output=None, 17 | output_file=None, 18 | output_dir=None, 19 | ): 20 | super().__init__() 21 | self.parent = parent 22 | 23 | # Create a labeling widget 24 | view = LabelingWidget( 25 | self, 26 | config=config, 27 | filename=filename, 28 | output=output, 29 | output_file=output_file, 30 | output_dir=output_dir, 31 | ) 32 | 33 | # Create the main layout and put labeling into 34 | main_layout = QVBoxLayout() 35 | main_layout.setContentsMargins(0, 0, 0, 0) 36 | main_layout.addWidget(view) 37 | self.setLayout(main_layout) 38 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/logger.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | import termcolor 5 | 6 | 7 | COLORS = { 8 | "WARNING": "yellow", 9 | "INFO": "white", 10 | "DEBUG": "blue", 11 | "CRITICAL": "red", 12 | "ERROR": "red", 13 | } 14 | 15 | 16 | class ColoredFormatter(logging.Formatter): 17 | def __init__(self, fmt, use_color=True): 18 | logging.Formatter.__init__(self, fmt) 19 | self.use_color = use_color 20 | 21 | def format(self, record): 22 | levelname = record.levelname 23 | if self.use_color and levelname in COLORS: 24 | 25 | def colored(text): 26 | return termcolor.colored( 27 | text, 28 | color=COLORS[levelname], 29 | attrs={"bold": True}, 30 | ) 31 | 32 | record.levelname2 = colored(f"{record.levelname:<7}") 33 | record.message2 = colored(record.msg) 34 | 35 | asctime2 = datetime.datetime.fromtimestamp(record.created) 36 | record.asctime2 = termcolor.colored(asctime2, color="green") 37 | 38 | record.module2 = termcolor.colored(record.module, color="cyan") 39 | record.funcName2 = termcolor.colored(record.funcName, color="cyan") 40 | record.lineno2 = termcolor.colored(record.lineno, color="cyan") 41 | return logging.Formatter.format(self, record) 42 | 43 | 44 | class ColoredLogger(logging.Logger): 45 | FORMAT = "[%(levelname2)s] %(module2)s:%(funcName2)s:%(lineno2)s - %(message2)s" 46 | 47 | def __init__(self, name): 48 | logging.Logger.__init__(self, name, logging.INFO) 49 | 50 | color_formatter = ColoredFormatter(self.FORMAT) 51 | 52 | console = logging.StreamHandler() 53 | console.setFormatter(color_formatter) 54 | 55 | self.addHandler(console) 56 | 57 | 58 | logger = logging.getLogger("AnyLabeling") 59 | logger.__class__ = ColoredLogger 60 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/testing.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path as osp 3 | 4 | import imgviz 5 | 6 | from . import utils 7 | 8 | 9 | def assert_labelfile_sanity(filename): 10 | assert osp.exists(filename) 11 | 12 | with open(filename) as f: 13 | data = json.load(f) 14 | 15 | assert "image_path" in data 16 | image_data = data.get("image_data", None) 17 | if image_data is None: 18 | parent_dir = osp.dirname(filename) 19 | img_file = osp.join(parent_dir, data["image_path"]) 20 | assert osp.exists(img_file) 21 | img = imgviz.io.imread(img_file) 22 | else: 23 | img = utils.img_b64_to_arr(image_data) 24 | 25 | H, W = img.shape[:2] 26 | assert H == data["image_height"] 27 | assert W == data["image_width"] 28 | 29 | assert "shapes" in data 30 | for shape in data["shapes"]: 31 | assert "label" in shape 32 | assert "points" in shape 33 | for x, y in shape["points"]: 34 | assert 0 <= x <= W 35 | assert 0 <= y <= H 36 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from ._io import lblsave 4 | from .image import ( 5 | apply_exif_orientation, 6 | img_arr_to_b64, 7 | img_b64_to_arr, 8 | img_data_to_arr, 9 | img_data_to_pil, 10 | img_data_to_png_data, 11 | img_pil_to_data, 12 | ) 13 | from .qt import ( 14 | Struct, 15 | add_actions, 16 | distance, 17 | distance_to_line, 18 | squared_distance_to_line, 19 | fmt_shortcut, 20 | label_validator, 21 | new_action, 22 | new_button, 23 | new_icon, 24 | ) 25 | from .shape import ( 26 | masks_to_bboxes, 27 | polygons_to_mask, 28 | shape_to_mask, 29 | shapes_to_label, 30 | ) 31 | 32 | # Export utilities 33 | from .export_formats import FormatExporter 34 | from .export_worker import ExportSignals, ExportWorker 35 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/_io.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | 3 | import imgviz 4 | import numpy as np 5 | import PIL.Image 6 | 7 | 8 | def lblsave(filename, lbl): 9 | if osp.splitext(filename)[1] != ".png": 10 | filename += ".png" 11 | # Assume label ranses [-1, 254] for int32, 12 | # and [0, 255] for uint8 as VOC. 13 | if lbl.min() >= -1 and lbl.max() < 255: 14 | lbl_pil = PIL.Image.fromarray(lbl.astype(np.uint8), mode="P") 15 | colormap = imgviz.label_colormap() 16 | lbl_pil.putpalette(colormap.flatten()) 17 | lbl_pil.save(filename) 18 | else: 19 | raise ValueError( 20 | f"[{filename}] Cannot save the pixel-wise class label as PNG. " 21 | "Please consider using the .npy format." 22 | ) 23 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import io 3 | 4 | import numpy as np 5 | import PIL.ExifTags 6 | import PIL.Image 7 | import PIL.ImageOps 8 | 9 | 10 | def img_data_to_pil(img_data): 11 | f = io.BytesIO() 12 | f.write(img_data) 13 | img_pil = PIL.Image.open(f) 14 | return img_pil 15 | 16 | 17 | def img_data_to_arr(img_data): 18 | img_pil = img_data_to_pil(img_data) 19 | img_arr = np.array(img_pil) 20 | return img_arr 21 | 22 | 23 | def img_b64_to_arr(img_b64): 24 | img_data = base64.b64decode(img_b64) 25 | img_arr = img_data_to_arr(img_data) 26 | return img_arr 27 | 28 | 29 | def img_pil_to_data(img_pil): 30 | f = io.BytesIO() 31 | img_pil.save(f, format="PNG") 32 | img_data = f.getvalue() 33 | return img_data 34 | 35 | 36 | def img_arr_to_b64(img_arr): 37 | img_pil = PIL.Image.fromarray(img_arr) 38 | f = io.BytesIO() 39 | img_pil.save(f, format="PNG") 40 | img_bin = f.getvalue() 41 | if hasattr(base64, "encodebytes"): 42 | img_b64 = base64.encodebytes(img_bin) 43 | else: 44 | img_b64 = base64.encodestring(img_bin) 45 | return img_b64 46 | 47 | 48 | def img_data_to_png_data(img_data): 49 | with io.BytesIO() as f: 50 | f.write(img_data) 51 | img = PIL.Image.open(f) 52 | 53 | with io.BytesIO() as f: 54 | img.save(f, "PNG") 55 | f.seek(0) 56 | return f.read() 57 | 58 | 59 | def apply_exif_orientation(image): 60 | try: 61 | exif = image._getexif() 62 | except AttributeError: 63 | exif = None 64 | 65 | if exif is None: 66 | return image 67 | 68 | exif = {PIL.ExifTags.TAGS[k]: v for k, v in exif.items() if k in PIL.ExifTags.TAGS} 69 | 70 | orientation = exif.get("Orientation", None) 71 | 72 | if orientation == 1: 73 | # do nothing 74 | return image 75 | if orientation == 2: 76 | # left-to-right mirror 77 | return PIL.ImageOps.mirror(image) 78 | if orientation == 3: 79 | # rotate 180 80 | return image.transpose(PIL.Image.ROTATE_180) 81 | if orientation == 4: 82 | # top-to-bottom mirror 83 | return PIL.ImageOps.flip(image) 84 | if orientation == 5: 85 | # top-to-left mirror 86 | return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_270)) 87 | if orientation == 6: 88 | # rotate 270 89 | return image.transpose(PIL.Image.ROTATE_270) 90 | if orientation == 7: 91 | # top-to-right mirror 92 | return PIL.ImageOps.mirror(image.transpose(PIL.Image.ROTATE_90)) 93 | if orientation == 8: 94 | # rotate 90 95 | return image.transpose(PIL.Image.ROTATE_90) 96 | return image 97 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/opencv.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | import cv2 4 | import numpy as np 5 | import qimage2ndarray 6 | from PyQt5 import QtGui 7 | from PyQt5.QtGui import QImage 8 | 9 | 10 | def qt_img_to_rgb_cv_img(qt_img, img_path=None): 11 | """ 12 | Convert 8bit/16bit RGB image or 8bit/16bit Gray image to 8bit RGB image 13 | """ 14 | if img_path is not None and os.path.exists(img_path): 15 | # Load Image From Path Directly 16 | cv_image = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), -1) 17 | cv_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB) 18 | else: 19 | if ( 20 | qt_img.format() == QImage.Format_RGB32 21 | or qt_img.format() == QImage.Format_ARGB32 22 | or qt_img.format() == QImage.Format_ARGB32_Premultiplied 23 | ): 24 | cv_image = qimage2ndarray.rgb_view(qt_img) 25 | else: 26 | cv_image = qimage2ndarray.raw_view(qt_img) 27 | # To uint8 28 | if cv_image.dtype != np.uint8: 29 | cv2.normalize(cv_image, cv_image, 0, 255, cv2.NORM_MINMAX) 30 | cv_image = np.array(cv_image, dtype=np.uint8) 31 | # To RGB 32 | if len(cv_image.shape) == 2 or cv_image.shape[2] == 1: 33 | cv_image = cv2.merge([cv_image, cv_image, cv_image]) 34 | return cv_image 35 | 36 | 37 | def qt_img_to_cv_img(in_image): 38 | return qimage2ndarray.rgb_view(in_image) 39 | 40 | 41 | def cv_img_to_qt_img(in_mat): 42 | return QtGui.QImage(qimage2ndarray.array2qimage(in_mat)) 43 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/qt.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | from math import hypot, sqrt 3 | 4 | import numpy as np 5 | from PyQt5 import QtCore, QtGui, QtWidgets 6 | 7 | here = osp.dirname(osp.abspath(__file__)) 8 | 9 | 10 | def new_icon(icon): 11 | return QtGui.QIcon(osp.join(f":/images/images/{icon}.png")) 12 | 13 | 14 | def new_button(text, icon=None, slot=None): 15 | b = QtWidgets.QPushButton(text) 16 | if icon is not None: 17 | b.setIcon(new_icon(icon)) 18 | if slot is not None: 19 | b.clicked.connect(slot) 20 | return b 21 | 22 | 23 | def new_action( 24 | parent, 25 | text, 26 | slot=None, 27 | shortcut=None, 28 | icon=None, 29 | tip=None, 30 | checkable=False, 31 | enabled=True, 32 | checked=False, 33 | ): 34 | """Create a new action and assign callbacks, shortcuts, etc.""" 35 | action = QtWidgets.QAction(text, parent) 36 | if icon is not None: 37 | action.setIconText(text.replace(" ", "\n")) 38 | action.setIcon(new_icon(icon)) 39 | if shortcut is not None: 40 | if isinstance(shortcut, (list, tuple)): 41 | action.setShortcuts(shortcut) 42 | else: 43 | action.setShortcut(shortcut) 44 | if tip is not None: 45 | action.setToolTip(tip) 46 | action.setStatusTip(tip) 47 | if slot is not None: 48 | action.triggered.connect(slot) 49 | if checkable: 50 | action.setCheckable(True) 51 | action.setEnabled(enabled) 52 | action.setChecked(checked) 53 | return action 54 | 55 | 56 | def add_actions(widget, actions): 57 | for action in actions: 58 | if action is None: 59 | widget.addSeparator() 60 | elif isinstance(action, QtWidgets.QMenu): 61 | widget.addMenu(action) 62 | else: 63 | widget.addAction(action) 64 | 65 | 66 | def label_validator(): 67 | return QtGui.QRegularExpressionValidator( 68 | QtCore.QRegularExpression(r"^[^ \t].+"), None 69 | ) 70 | 71 | 72 | class Struct: 73 | def __init__(self, **kwargs): 74 | self.__dict__.update(kwargs) 75 | 76 | 77 | def distance(p): 78 | return sqrt(p.x() * p.x() + p.y() * p.y()) 79 | 80 | 81 | def distance_to_line(point, line): 82 | p1, p2 = line 83 | p1 = np.array([p1.x(), p1.y()]) 84 | p2 = np.array([p2.x(), p2.y()]) 85 | p3 = np.array([point.x(), point.y()]) 86 | if np.dot((p3 - p1), (p2 - p1)) < 0: 87 | return np.linalg.norm(p3 - p1) 88 | if np.dot((p3 - p2), (p1 - p2)) < 0: 89 | return np.linalg.norm(p3 - p2) 90 | if np.linalg.norm(p2 - p1) == 0: 91 | return 0 92 | return np.linalg.norm(np.cross(p2 - p1, p1 - p3)) / np.linalg.norm(p2 - p1) 93 | 94 | 95 | def squared_distance_to_line(point, line): 96 | """ 97 | Use python math because it is faster than using numpy 98 | """ 99 | p1, p2 = line 100 | px, py = point.x(), point.y() 101 | x1, y1 = p1.x(), p1.y() 102 | x2, y2 = p2.x(), p2.y() 103 | 104 | dx, dy = x2 - x1, y2 - y1 105 | if dx == dy == 0: 106 | return hypot(px - x1, py - y1) 107 | 108 | # Calculate the projection and check if it falls on the line segment 109 | t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy) 110 | if t < 0: 111 | dx, dy = px - x1, py - y1 # point to p1 112 | elif t > 1: 113 | dx, dy = px - x2, py - y2 # point to p2 114 | else: 115 | near_x, near_y = x1 + t * dx, y1 + t * dy 116 | dx, dy = px - near_x, py - near_y 117 | 118 | return hypot(dx, dy) 119 | 120 | 121 | def fmt_shortcut(text): 122 | mod, key = text.split("+", 1) 123 | return f"{mod}+{key}" 124 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/utils/shape.py: -------------------------------------------------------------------------------- 1 | import math 2 | import uuid 3 | 4 | import numpy as np 5 | import PIL.Image 6 | import PIL.ImageDraw 7 | 8 | from ..logger import logger 9 | 10 | 11 | def polygons_to_mask(img_shape, polygons, shape_type=None): 12 | logger.warning( 13 | "The 'polygons_to_mask' function is deprecated, use 'shape_to_mask' instead." 14 | ) 15 | return shape_to_mask(img_shape, points=polygons, shape_type=shape_type) 16 | 17 | 18 | def shape_to_mask(img_shape, points, shape_type=None, line_width=10, point_size=5): 19 | mask = np.zeros(img_shape[:2], dtype=np.uint8) 20 | mask = PIL.Image.fromarray(mask) 21 | draw = PIL.ImageDraw.Draw(mask) 22 | xy = [tuple(point) for point in points] 23 | if shape_type == "circle": 24 | assert len(xy) == 2, "Shape of shape_type=circle must have 2 points" 25 | (cx, cy), (px, py) = xy 26 | d = math.sqrt((cx - px) ** 2 + (cy - py) ** 2) 27 | draw.ellipse([cx - d, cy - d, cx + d, cy + d], outline=1, fill=1) 28 | elif shape_type == "rectangle": 29 | assert len(xy) == 2, "Shape of shape_type=rectangle must have 2 points" 30 | draw.rectangle(xy, outline=1, fill=1) 31 | elif shape_type == "line": 32 | assert len(xy) == 2, "Shape of shape_type=line must have 2 points" 33 | draw.line(xy=xy, fill=1, width=line_width) 34 | elif shape_type == "linestrip": 35 | draw.line(xy=xy, fill=1, width=line_width) 36 | elif shape_type == "point": 37 | assert len(xy) == 1, "Shape of shape_type=point must have 1 points" 38 | cx, cy = xy[0] 39 | r = point_size 40 | draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=1, fill=1) 41 | else: 42 | assert len(xy) > 2, "Polygon must have points more than 2" 43 | draw.polygon(xy=xy, outline=1, fill=1) 44 | mask = np.array(mask, dtype=bool) 45 | return mask 46 | 47 | 48 | def shapes_to_label(img_shape, shapes, label_name_to_value): 49 | cls = np.zeros(img_shape[:2], dtype=np.int32) 50 | ins = np.zeros_like(cls) 51 | instances = [] 52 | for shape in shapes: 53 | points = shape["points"] 54 | label = shape["label"] 55 | group_id = shape.get("group_id") 56 | if group_id is None: 57 | group_id = uuid.uuid1() 58 | shape_type = shape.get("shape_type", None) 59 | 60 | cls_name = label 61 | instance = (cls_name, group_id) 62 | 63 | if instance not in instances: 64 | instances.append(instance) 65 | ins_id = instances.index(instance) + 1 66 | cls_id = label_name_to_value[cls_name] 67 | 68 | mask = shape_to_mask(img_shape[:2], points, shape_type) 69 | cls[mask] = cls_id 70 | ins[mask] = ins_id 71 | 72 | return cls, ins 73 | 74 | 75 | def masks_to_bboxes(masks): 76 | if masks.ndim != 3: 77 | raise ValueError(f"masks.ndim must be 3, but it is {masks.ndim}") 78 | if masks.dtype != bool: 79 | raise ValueError(f"masks.dtype must be bool type, but it is {masks.dtype}") 80 | bboxes = [] 81 | for mask in masks: 82 | where = np.argwhere(mask) 83 | (y1, x1), (y2, x2) = where.min(0), where.max(0) + 1 84 | bboxes.append((y1, x1, y2, x2)) 85 | bboxes = np.asarray(bboxes, dtype=np.float32) 86 | return bboxes 87 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .auto_labeling import AutoLabelingWidget 4 | from .brightness_contrast_dialog import BrightnessContrastDialog 5 | from .canvas import Canvas 6 | from .color_dialog import ColorDialog 7 | from .file_dialog_preview import FileDialogPreview 8 | from .label_dialog import LabelDialog, LabelQLineEdit 9 | from .label_list_widget import LabelListWidget, LabelListWidgetItem 10 | from .toolbar import ToolBar 11 | from .unique_label_qlist_widget import UniqueLabelQListWidget 12 | from .zoom_widget import ZoomWidget 13 | 14 | # Package widgets 15 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/auto_labeling/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_labeling import AutoLabelingWidget 2 | 3 | __all__ = ["AutoLabelingWidget"] 4 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | auto_labeling_form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1118 10 | 68 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 29 | 30 | 2 31 | 32 | 33 | 2 34 | 35 | 36 | 2 37 | 38 | 39 | 2 40 | 41 | 42 | 43 | 44 | 2 45 | 46 | 47 | 4 48 | 49 | 50 | 0 51 | 52 | 53 | 54 | 55 | Auto 56 | 57 | 58 | 59 | 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 200 67 | 0 68 | 69 | 70 | 71 | 72 | No Model 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Output 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | polygon 89 | 90 | 91 | 92 | 93 | rectangle 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Qt::Vertical 102 | 103 | 104 | 105 | 106 | 107 | 108 | Run (i) 109 | 110 | 111 | 112 | 113 | 114 | 115 | +Point (Q) 116 | 117 | 118 | 119 | 120 | 121 | 122 | -Point (E) 123 | 124 | 125 | 126 | 127 | 128 | 129 | +Rect 130 | 131 | 132 | 133 | 134 | 135 | 136 | Clear 137 | 138 | 139 | 140 | 141 | 142 | 143 | Finish Object (f) 144 | 145 | 146 | 147 | 148 | 149 | 150 | Qt::Horizontal 151 | 152 | 153 | 154 | 0 155 | 0 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 0 165 | 0 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | :/images/images/cancel.png:/images/images/cancel.png 174 | 175 | 176 | 177 | 12 178 | 12 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 0 190 | 0 191 | 192 | 193 | 194 | 195 | 10 196 | 197 | 198 | 199 | margin-top: 0; 200 | margin-bottom: 10px; 201 | 202 | 203 | Ready! 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/brightness_contrast_dialog.py: -------------------------------------------------------------------------------- 1 | """This module defines brightness/contrast dialog""" 2 | 3 | import PIL.Image 4 | import PIL.ImageEnhance 5 | from PyQt5 import QtGui, QtWidgets 6 | from PyQt5.QtCore import Qt 7 | 8 | from .. import utils 9 | 10 | 11 | class BrightnessContrastDialog(QtWidgets.QDialog): 12 | """Dialog for adjusting brightness and contrast of current image""" 13 | 14 | def __init__(self, img, callback, parent=None): 15 | super(BrightnessContrastDialog, self).__init__(parent) 16 | self.setModal(True) 17 | self.setWindowTitle(self.tr("Brightness/Contrast")) 18 | 19 | self.slider_brightness = self._create_slider() 20 | self.slider_contrast = self._create_slider() 21 | 22 | form_layout = QtWidgets.QFormLayout() 23 | form_layout.addRow(self.tr("Brightness"), self.slider_brightness) 24 | form_layout.addRow(self.tr("Contrast"), self.slider_contrast) 25 | self.setLayout(form_layout) 26 | 27 | assert isinstance(img, PIL.Image.Image) 28 | self.img = img 29 | self.callback = callback 30 | 31 | def on_new_value(self, value): 32 | """On new value event""" 33 | brightness = self.slider_brightness.value() / 50.0 34 | contrast = self.slider_contrast.value() / 50.0 35 | 36 | img = self.img 37 | img = PIL.ImageEnhance.Brightness(img).enhance(brightness) 38 | img = PIL.ImageEnhance.Contrast(img).enhance(contrast) 39 | 40 | img_data = utils.img_pil_to_data(img) 41 | qimage = QtGui.QImage.fromData(img_data) 42 | self.callback(qimage) 43 | 44 | def _create_slider(self): 45 | """Create brightness/contrast slider""" 46 | slider = QtWidgets.QSlider(Qt.Horizontal) 47 | slider.setRange(0, 150) 48 | slider.setValue(50) 49 | slider.valueChanged.connect(self.on_new_value) 50 | return slider 51 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/color_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets 2 | 3 | 4 | class ColorDialog(QtWidgets.QColorDialog): 5 | def __init__(self, parent=None): 6 | super(ColorDialog, self).__init__(parent) 7 | self.setOption(QtWidgets.QColorDialog.ShowAlphaChannel) 8 | # The Mac native dialog does not support our restore button. 9 | self.setOption(QtWidgets.QColorDialog.DontUseNativeDialog) 10 | # Add a restore defaults button. 11 | # The default is set at invocation time, so that it 12 | # works across dialogs for different elements. 13 | self.default = None 14 | self.bb = self.layout().itemAt(1).widget() 15 | self.bb.addButton(QtWidgets.QDialogButtonBox.RestoreDefaults) 16 | self.bb.clicked.connect(self.check_restore) 17 | 18 | def get_color(self, value=None, title=None, default=None): 19 | self.default = default 20 | if title: 21 | self.setWindowTitle(title) 22 | if value: 23 | self.setCurrentColor(value) 24 | return self.currentColor() if self.exec_() else None 25 | 26 | def check_restore(self, button): 27 | if ( 28 | self.bb.buttonRole(button) & QtWidgets.QDialogButtonBox.ResetRole 29 | and self.default 30 | ): 31 | self.setCurrentColor(self.default) 32 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/escapable_qlist_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtWidgets 2 | from PyQt5.QtCore import Qt 3 | 4 | 5 | class EscapableQListWidget(QtWidgets.QListWidget): 6 | # QT Overload 7 | def keyPressEvent(self, event): 8 | super(EscapableQListWidget, self).keyPressEvent(event) 9 | if event.key() == Qt.Key_Escape: 10 | self.clearSelection() 11 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/file_dialog_preview.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | 5 | 6 | class ScrollAreaPreview(QtWidgets.QScrollArea): 7 | def __init__(self, *args, **kwargs): 8 | super(ScrollAreaPreview, self).__init__(*args, **kwargs) 9 | 10 | self.setWidgetResizable(True) 11 | 12 | content = QtWidgets.QWidget(self) 13 | self.setWidget(content) 14 | 15 | layout = QtWidgets.QVBoxLayout(content) 16 | 17 | self.label = QtWidgets.QLabel(content) 18 | self.label.setWordWrap(True) 19 | 20 | layout.addWidget(self.label) 21 | 22 | def set_text(self, text): 23 | self.label.setText(text) 24 | 25 | def set_pixmap(self, pixmap): 26 | self.label.setPixmap(pixmap) 27 | 28 | def clear(self): 29 | self.label.clear() 30 | 31 | 32 | class FileDialogPreview(QtWidgets.QFileDialog): 33 | def __init__(self, *args, **kwargs): 34 | super(FileDialogPreview, self).__init__(*args, **kwargs) 35 | self.setOption(self.DontUseNativeDialog, True) 36 | 37 | self.label_preview = ScrollAreaPreview(self) 38 | self.label_preview.setFixedSize(300, 300) 39 | self.label_preview.setHidden(True) 40 | 41 | box = QtWidgets.QVBoxLayout() 42 | box.addWidget(self.label_preview) 43 | box.addStretch() 44 | 45 | self.setFixedSize(self.width() + 300, self.height()) 46 | self.layout().addLayout(box, 1, 3, 1, 1) 47 | self.currentChanged.connect(self.on_change) 48 | 49 | def on_change(self, path): 50 | if path.lower().endswith(".json"): 51 | with open(path, "r") as f: 52 | data = json.load(f) 53 | self.label_preview.set_text(json.dumps(data, indent=4, sort_keys=False)) 54 | self.label_preview.label.setAlignment( 55 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop 56 | ) 57 | self.label_preview.setHidden(False) 58 | else: 59 | pixmap = QtGui.QPixmap(path) 60 | if pixmap.isNull(): 61 | self.label_preview.clear() 62 | self.label_preview.setHidden(True) 63 | else: 64 | self.label_preview.set_pixmap( 65 | pixmap.scaled( 66 | self.label_preview.width() - 30, 67 | self.label_preview.height() - 30, 68 | QtCore.Qt.KeepAspectRatio, 69 | QtCore.Qt.SmoothTransformation, 70 | ) 71 | ) 72 | self.label_preview.label.setAlignment(QtCore.Qt.AlignCenter) 73 | self.label_preview.setHidden(False) 74 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/label_dialog.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from PyQt5 import QtCore, QtGui, QtWidgets 4 | from PyQt5.QtCore import QCoreApplication 5 | 6 | from .. import utils 7 | from ..logger import logger 8 | 9 | # TODO(unknown): 10 | # - Calculate optimal position so as not to go out of screen area. 11 | 12 | 13 | class LabelQLineEdit(QtWidgets.QLineEdit): 14 | def __init__(self) -> None: 15 | super().__init__() 16 | self.list_widget = None 17 | 18 | def set_list_widget(self, list_widget): 19 | self.list_widget = list_widget 20 | 21 | # QT Overload 22 | def keyPressEvent(self, e): 23 | if e.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Down]: 24 | self.list_widget.keyPressEvent(e) 25 | else: 26 | super(LabelQLineEdit, self).keyPressEvent(e) 27 | 28 | 29 | class LabelDialog(QtWidgets.QDialog): 30 | def __init__( 31 | self, 32 | text=None, 33 | parent=None, 34 | labels=None, 35 | sort_labels=True, 36 | show_text_field=True, 37 | completion="startswith", 38 | fit_to_content=None, 39 | flags=None, 40 | ): 41 | if text is None: 42 | text = QCoreApplication.translate("LabelDialog", "Enter object label") 43 | 44 | if fit_to_content is None: 45 | fit_to_content = {"row": False, "column": True} 46 | self._fit_to_content = fit_to_content 47 | 48 | super(LabelDialog, self).__init__(parent) 49 | self.edit = LabelQLineEdit() 50 | self.edit.setPlaceholderText(text) 51 | self.edit.setValidator(utils.label_validator()) 52 | self.edit.editingFinished.connect(self.postprocess) 53 | if flags: 54 | self.edit.textChanged.connect(self.update_flags) 55 | self.edit_group_id = QtWidgets.QLineEdit() 56 | self.edit_group_id.setPlaceholderText(self.tr("Group ID")) 57 | self.edit_group_id.setValidator( 58 | QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(r"\d*"), None) 59 | ) 60 | layout = QtWidgets.QVBoxLayout() 61 | layout.setContentsMargins(10, 10, 10, 10) 62 | if show_text_field: 63 | layout_edit = QtWidgets.QHBoxLayout() 64 | layout_edit.addWidget(self.edit, 6) 65 | layout_edit.addWidget(self.edit_group_id, 2) 66 | layout.addLayout(layout_edit) 67 | # buttons 68 | self.button_box = bb = QtWidgets.QDialogButtonBox( 69 | QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, 70 | QtCore.Qt.Horizontal, 71 | self, 72 | ) 73 | bb.button(bb.Ok).setIcon(utils.new_icon("done")) 74 | bb.button(bb.Cancel).setIcon(utils.new_icon("undo")) 75 | bb.accepted.connect(self.validate) 76 | bb.rejected.connect(self.reject) 77 | layout.addWidget(bb) 78 | # label_list 79 | self.label_list = QtWidgets.QListWidget() 80 | if self._fit_to_content["row"]: 81 | self.label_list.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 82 | if self._fit_to_content["column"]: 83 | self.label_list.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 84 | self._sort_labels = sort_labels 85 | if labels: 86 | self.label_list.addItems(labels) 87 | if self._sort_labels: 88 | self.label_list.sortItems() 89 | else: 90 | self.label_list.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) 91 | self.label_list.currentItemChanged.connect(self.label_selected) 92 | self.label_list.itemDoubleClicked.connect(self.label_double_clicked) 93 | self.edit.set_list_widget(self.label_list) 94 | layout.addWidget(self.label_list) 95 | # label_flags 96 | if flags is None: 97 | flags = {} 98 | self._flags = flags 99 | self.flags_layout = QtWidgets.QVBoxLayout() 100 | self.reset_flags() 101 | layout.addItem(self.flags_layout) 102 | self.edit.textChanged.connect(self.update_flags) 103 | self.setLayout(layout) 104 | # completion 105 | completer = QtWidgets.QCompleter() 106 | if completion == "startswith": 107 | completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion) 108 | # Default settings. 109 | # completer.setFilterMode(QtCore.Qt.MatchStartsWith) 110 | elif completion == "contains": 111 | completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion) 112 | completer.setFilterMode(QtCore.Qt.MatchContains) 113 | else: 114 | raise ValueError(f"Unsupported completion: {completion}") 115 | completer.setModel(self.label_list.model()) 116 | self.edit.setCompleter(completer) 117 | # Save last label 118 | self._last_label = "" 119 | 120 | def get_last_label(self): 121 | return self._last_label 122 | 123 | def add_label_history(self, label): 124 | self._last_label = label 125 | if self.label_list.findItems(label, QtCore.Qt.MatchExactly): 126 | return 127 | self.label_list.addItem(label) 128 | if self._sort_labels: 129 | self.label_list.sortItems() 130 | 131 | def label_selected(self, item): 132 | self.edit.setText(item.text()) 133 | 134 | def validate(self): 135 | text = self.edit.text() 136 | if hasattr(text, "strip"): 137 | text = text.strip() 138 | else: 139 | text = text.trimmed() 140 | if text: 141 | self.accept() 142 | 143 | def label_double_clicked(self, _): 144 | self.validate() 145 | 146 | def postprocess(self): 147 | text = self.edit.text() 148 | if hasattr(text, "strip"): 149 | text = text.strip() 150 | else: 151 | text = text.trimmed() 152 | self.edit.setText(text) 153 | 154 | def update_flags(self, label_new): 155 | # keep state of shared flags 156 | flags_old = self.get_flags() 157 | 158 | flags_new = {} 159 | for pattern, keys in self._flags.items(): 160 | if re.match(pattern, label_new): 161 | for key in keys: 162 | flags_new[key] = flags_old.get(key, False) 163 | self.set_flags(flags_new) 164 | 165 | def delete_flags(self): 166 | for i in reversed(range(self.flags_layout.count())): 167 | item = self.flags_layout.itemAt(i).widget() 168 | self.flags_layout.removeWidget(item) 169 | item.setParent(None) 170 | 171 | def reset_flags(self, label=""): 172 | flags = {} 173 | for pattern, keys in self._flags.items(): 174 | if re.match(pattern, label): 175 | for key in keys: 176 | flags[key] = False 177 | self.set_flags(flags) 178 | 179 | def set_flags(self, flags): 180 | self.delete_flags() 181 | for key in flags: 182 | item = QtWidgets.QCheckBox(key, self) 183 | item.setChecked(bool(flags[key])) 184 | self.flags_layout.addWidget(item) 185 | item.show() 186 | 187 | def get_flags(self): 188 | flags = {} 189 | for i in range(self.flags_layout.count()): 190 | item = self.flags_layout.itemAt(i).widget() 191 | flags[item.text()] = item.isChecked() 192 | return flags 193 | 194 | def get_group_id(self): 195 | group_id = self.edit_group_id.text() 196 | if group_id: 197 | return int(group_id) 198 | return None 199 | 200 | def pop_up(self, text=None, move=True, flags=None, group_id=None): 201 | if self._fit_to_content["row"]: 202 | self.label_list.setMinimumHeight( 203 | self.label_list.sizeHintForRow(0) * self.label_list.count() + 2 204 | ) 205 | if self._fit_to_content["column"]: 206 | self.label_list.setMinimumWidth(self.label_list.sizeHintForColumn(0) + 2) 207 | # if text is None, the previous label in self.edit is kept 208 | if text is None: 209 | text = self.edit.text() 210 | if flags: 211 | self.set_flags(flags) 212 | else: 213 | self.reset_flags(text) 214 | self.edit.setText(text) 215 | self.edit.setSelection(0, len(text)) 216 | if group_id is None: 217 | self.edit_group_id.clear() 218 | else: 219 | self.edit_group_id.setText(str(group_id)) 220 | items = self.label_list.findItems(text, QtCore.Qt.MatchFixedString) 221 | if items: 222 | if len(items) != 1: 223 | logger.warning("Label list has duplicate '%s'", text) 224 | self.label_list.setCurrentItem(items[0]) 225 | row = self.label_list.row(items[0]) 226 | self.edit.completer().setCurrentRow(row) 227 | self.edit.setFocus(QtCore.Qt.PopupFocusReason) 228 | if move: 229 | self.move(QtGui.QCursor.pos()) 230 | if self.exec_(): 231 | return self.edit.text(), self.get_flags(), self.get_group_id() 232 | 233 | return None, None, None 234 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/label_list_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | from PyQt5.QtCore import Qt 3 | from PyQt5.QtGui import QPalette 4 | from PyQt5.QtWidgets import QStyle 5 | 6 | 7 | # https://stackoverflow.com/a/2039745/4158863 8 | class HTMLDelegate(QtWidgets.QStyledItemDelegate): 9 | def __init__(self, parent=None): 10 | self.parent = parent 11 | super(HTMLDelegate, self).__init__() 12 | self.doc = QtGui.QTextDocument(self) 13 | 14 | def paint(self, painter, option, index): 15 | painter.save() 16 | 17 | options = QtWidgets.QStyleOptionViewItem(option) 18 | 19 | self.initStyleOption(options, index) 20 | self.doc.setHtml(options.text) 21 | options.text = "" 22 | 23 | style = ( 24 | QtWidgets.QApplication.style() 25 | if options.widget is None 26 | else options.widget.style() 27 | ) 28 | style.drawControl(QStyle.CE_ItemViewItem, options, painter) 29 | 30 | ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() 31 | 32 | if option.state & QStyle.State_Selected: 33 | ctx.palette.setColor( 34 | QPalette.Text, 35 | option.palette.color(QPalette.Active, QPalette.HighlightedText), 36 | ) 37 | else: 38 | ctx.palette.setColor( 39 | QPalette.Text, 40 | option.palette.color(QPalette.Active, QPalette.Text), 41 | ) 42 | 43 | text_rect = style.subElementRect(QStyle.SE_ItemViewItemText, options) 44 | 45 | if index.column() != 0: 46 | text_rect.adjust(5, 0, 0, 0) 47 | 48 | margin_constant = 4 49 | margin = (option.rect.height() - options.fontMetrics.height()) // 2 50 | margin = margin - margin_constant 51 | text_rect.setTop(text_rect.top() + margin) 52 | 53 | painter.translate(text_rect.topLeft()) 54 | painter.setClipRect(text_rect.translated(-text_rect.topLeft())) 55 | self.doc.documentLayout().draw(painter, ctx) 56 | 57 | painter.restore() 58 | 59 | # QT Overload 60 | def sizeHint(self, _, _2): 61 | margin_constant = 4 62 | return QtCore.QSize( 63 | int(self.doc.idealWidth()), 64 | int(self.doc.size().height() - margin_constant), 65 | ) 66 | 67 | 68 | class LabelListWidgetItem(QtGui.QStandardItem): 69 | def __init__(self, text=None, shape=None): 70 | super(LabelListWidgetItem, self).__init__() 71 | self.setText(text or "") 72 | self.set_shape(shape) 73 | 74 | self.setCheckable(True) 75 | self.setCheckState(Qt.Checked) 76 | self.setEditable(False) 77 | self.setTextAlignment(Qt.AlignBottom) 78 | 79 | def clone(self): 80 | return LabelListWidgetItem(self.text(), self.shape()) 81 | 82 | def set_shape(self, shape): 83 | self.setData(shape, Qt.UserRole) 84 | 85 | def shape(self): 86 | return self.data(Qt.UserRole) 87 | 88 | def __hash__(self): 89 | return id(self) 90 | 91 | def __repr__(self): 92 | return f'{self.__class__.__name__}("{self.text()!r}")' 93 | 94 | 95 | class StandardItemModel(QtGui.QStandardItemModel): 96 | itemDropped = QtCore.pyqtSignal() 97 | 98 | # QT Overload 99 | def removeRows(self, *args, **kwargs): 100 | ret = super().removeRows(*args, **kwargs) 101 | self.itemDropped.emit() 102 | return ret 103 | 104 | 105 | class LabelListWidget(QtWidgets.QListView): 106 | item_double_clicked = QtCore.pyqtSignal(LabelListWidgetItem) 107 | item_selection_changed = QtCore.pyqtSignal(list, list) 108 | 109 | def __init__(self): 110 | super().__init__() 111 | self._selected_items = [] 112 | 113 | self.setWindowFlags(Qt.Window) 114 | self.setModel(StandardItemModel()) 115 | self.model().setItemPrototype(LabelListWidgetItem()) 116 | self.setItemDelegate(HTMLDelegate()) 117 | self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) 118 | self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) 119 | self.setDefaultDropAction(Qt.MoveAction) 120 | 121 | self.doubleClicked.connect(self.item_double_clicked_event) 122 | self.selectionModel().selectionChanged.connect( 123 | self.item_selection_changed_event 124 | ) 125 | 126 | def __len__(self): 127 | return self.model().rowCount() 128 | 129 | def __getitem__(self, i): 130 | return self.model().item(i) 131 | 132 | def __iter__(self): 133 | for i in range(len(self)): 134 | yield self[i] 135 | 136 | @property 137 | def item_dropped(self): 138 | return self.model().itemDropped 139 | 140 | @property 141 | def item_changed(self): 142 | return self.model().itemChanged 143 | 144 | def item_selection_changed_event(self, selected, deselected): 145 | selected = [self.model().itemFromIndex(i) for i in selected.indexes()] 146 | deselected = [self.model().itemFromIndex(i) for i in deselected.indexes()] 147 | self.item_selection_changed.emit(selected, deselected) 148 | 149 | def item_double_clicked_event(self, index): 150 | self.item_double_clicked.emit(self.model().itemFromIndex(index)) 151 | 152 | def selected_items(self): 153 | return [self.model().itemFromIndex(i) for i in self.selectedIndexes()] 154 | 155 | def scroll_to_item(self, item): 156 | self.scrollTo(self.model().indexFromItem(item)) 157 | 158 | def add_iem(self, item): 159 | if not isinstance(item, LabelListWidgetItem): 160 | raise TypeError("item must be LabelListWidgetItem") 161 | self.model().setItem(self.model().rowCount(), 0, item) 162 | item.setSizeHint(self.itemDelegate().sizeHint(None, None)) 163 | 164 | def remove_item(self, item): 165 | index = self.model().indexFromItem(item) 166 | self.model().removeRows(index.row(), 1) 167 | 168 | def select_item(self, item): 169 | index = self.model().indexFromItem(item) 170 | self.selectionModel().select(index, QtCore.QItemSelectionModel.Select) 171 | 172 | def find_item_by_shape(self, shape): 173 | for row in range(self.model().rowCount()): 174 | item = self.model().item(row, 0) 175 | if item.shape() == shape: 176 | return item 177 | raise ValueError(f"cannot find shape: {shape}") 178 | 179 | def clear(self): 180 | self.model().clear() 181 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/toolbar.py: -------------------------------------------------------------------------------- 1 | """Defines toolbar for anylabeling, including""" 2 | 3 | from PyQt5 import QtCore, QtWidgets 4 | from anylabeling.styles import AppTheme 5 | 6 | 7 | class ToolBar(QtWidgets.QToolBar): 8 | """Toolbar widget for labeling tool""" 9 | 10 | def __init__(self, title): 11 | super().__init__(title) 12 | layout = self.layout() 13 | margin = (0, 0, 0, 0) 14 | layout.setSpacing(0) 15 | layout.setContentsMargins(*margin) 16 | self.setContentsMargins(*margin) 17 | self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) 18 | 19 | # Use theme system for styling 20 | self.setStyleSheet( 21 | f""" 22 | QToolBar {{ 23 | background: {AppTheme.get_color("toolbar_bg")}; 24 | padding: 0px; 25 | border: 0px; 26 | border-radius: 5px; 27 | border: 2px solid {AppTheme.get_color("border")}; 28 | }} 29 | """ 30 | ) 31 | 32 | def add_action(self, action): 33 | """Add an action (button) to the toolbar""" 34 | if isinstance(action, QtWidgets.QWidgetAction): 35 | return super().addAction(action) 36 | btn = QtWidgets.QToolButton() 37 | btn.setDefaultAction(action) 38 | btn.setToolButtonStyle(self.toolButtonStyle()) 39 | self.addWidget(btn) 40 | 41 | # Center alignment 42 | for i in range(self.layout().count()): 43 | if isinstance(self.layout().itemAt(i).widget(), QtWidgets.QToolButton): 44 | self.layout().itemAt(i).setAlignment(QtCore.Qt.AlignCenter) 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/unique_label_qlist_widget.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import html 4 | 5 | from PyQt5 import QtWidgets 6 | from PyQt5.QtCore import Qt 7 | 8 | from .escapable_qlist_widget import EscapableQListWidget 9 | 10 | 11 | class UniqueLabelQListWidget(EscapableQListWidget): 12 | # QT Overload 13 | def mousePressEvent(self, event): 14 | super().mousePressEvent(event) 15 | if not self.indexAt(event.pos()).isValid(): 16 | self.clearSelection() 17 | 18 | def find_items_by_label(self, label): 19 | items = [] 20 | for row in range(self.count()): 21 | item = self.item(row) 22 | if item.data(Qt.UserRole) == label: 23 | items.append(item) 24 | return items 25 | 26 | def create_item_from_label(self, label): 27 | item = QtWidgets.QListWidgetItem() 28 | item.setData(Qt.UserRole, label) 29 | return item 30 | 31 | def set_item_label(self, item, label, color=None): 32 | qlabel = QtWidgets.QLabel() 33 | if color is None: 34 | qlabel.setText(f"{label}") 35 | else: 36 | qlabel.setText( 37 | '{} '.format( 38 | html.escape(label), *color 39 | ) 40 | ) 41 | qlabel.setAlignment(Qt.AlignBottom) 42 | item.setSizeHint(qlabel.sizeHint()) 43 | self.setItemWidget(item, qlabel) 44 | -------------------------------------------------------------------------------- /anylabeling/views/labeling/widgets/zoom_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | 3 | 4 | class ZoomWidget(QtWidgets.QSpinBox): 5 | def __init__(self, value=100): 6 | super().__init__() 7 | self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) 8 | self.setRange(1, 1000) 9 | self.setSuffix("%") 10 | self.setValue(value) 11 | self.setToolTip(self.tr("Zoom Level")) 12 | self.setStatusTip(self.toolTip()) 13 | self.setAlignment(QtCore.Qt.AlignCenter) 14 | font = self.font() 15 | font.setPointSize(9) 16 | self.setFont(font) 17 | 18 | # QT Overload 19 | def minimumSizeHint(self): 20 | height = super().minimumSizeHint().height() 21 | font_metric = QtGui.QFontMetrics(self.font()) 22 | width = font_metric.horizontalAdvance(str(self.maximum())) 23 | return QtCore.QSize(width, height) 24 | -------------------------------------------------------------------------------- /anylabeling/views/mainwindow.py: -------------------------------------------------------------------------------- 1 | """This module defines the main application window""" 2 | 3 | from PyQt5.QtWidgets import QMainWindow, QStatusBar, QVBoxLayout, QWidget 4 | 5 | from ..app_info import __appdescription__, __appname__ 6 | from .labeling.label_wrapper import LabelingWrapper 7 | 8 | 9 | class MainWindow(QMainWindow): 10 | """Main application window""" 11 | 12 | def __init__( 13 | self, 14 | app, 15 | config=None, 16 | filename=None, 17 | output=None, 18 | output_file=None, 19 | output_dir=None, 20 | ): 21 | super().__init__() 22 | self.app = app 23 | self.config = config 24 | 25 | self.setContentsMargins(0, 0, 0, 0) 26 | self.setWindowTitle(__appname__) 27 | 28 | main_layout = QVBoxLayout() 29 | main_layout.setContentsMargins(10, 10, 10, 10) 30 | self.labeling_widget = LabelingWrapper( 31 | self, 32 | config=config, 33 | filename=filename, 34 | output=output, 35 | output_file=output_file, 36 | output_dir=output_dir, 37 | ) 38 | main_layout.addWidget(self.labeling_widget) 39 | widget = QWidget() 40 | widget.setLayout(main_layout) 41 | self.setCentralWidget(widget) 42 | 43 | status_bar = QStatusBar() 44 | status_bar.showMessage(f"{__appname__} - {__appdescription__}") 45 | self.setStatusBar(status_bar) 46 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/assets/screenshot.png -------------------------------------------------------------------------------- /docs/macos_folder_mode.md: -------------------------------------------------------------------------------- 1 | # macOS Build for AnyLabeling 2 | 3 | ## Overview 4 | 5 | The macOS build of AnyLabeling is provided as a directory structure rather than a bundled `.app` file. This approach offers: 6 | 7 | - Easier integration with other tools or scripts 8 | - Customization of the application's resources 9 | - Better compatibility across different macOS versions 10 | - Direct access to application files 11 | 12 | ## Installation 13 | 14 | 1. Download the appropriate `AnyLabeling-Folder.zip` (CPU) or `AnyLabeling-Folder-GPU.zip` (GPU) from the [releases page](https://github.com/vietanhdev/anylabeling/releases). 15 | 16 | 2. Extract the downloaded ZIP file: 17 | ```bash 18 | unzip AnyLabeling-Folder.zip 19 | ``` 20 | 21 | 3. The extracted folder `AnyLabeling-Folder` contains everything needed to run the application. 22 | 23 | ## Running the Application 24 | 25 | To run the application, execute the `anylabeling` binary in the folder: 26 | 27 | ```bash 28 | cd AnyLabeling-Folder 29 | ./anylabeling 30 | ``` 31 | 32 | You can also create a shortcut or alias to this executable for easier access. 33 | 34 | ## Building Locally 35 | 36 | If you want to build the application yourself: 37 | 38 | 1. Clone the repository: 39 | ```bash 40 | git clone https://github.com/vietanhdev/anylabeling.git 41 | cd anylabeling 42 | ``` 43 | 44 | 2. Install dependencies: 45 | ```bash 46 | pip install -r requirements-macos-dev.txt 47 | ``` 48 | 49 | 3. Make the build script executable: 50 | ```bash 51 | chmod +x scripts/build_macos_folder.sh 52 | ``` 53 | 54 | 4. Run the build script: 55 | ```bash 56 | # For CPU version 57 | ./scripts/build_macos_folder.sh 58 | 59 | # For GPU version 60 | ./scripts/build_macos_folder.sh GPU 61 | ``` 62 | 63 | 5. The built application folder will be available at `./dist/AnyLabeling-Folder/` (CPU) or `./dist/AnyLabeling-Folder-GPU/` (GPU). 64 | 65 | ## Troubleshooting 66 | 67 | If you encounter issues with the application: 68 | 69 | - Ensure you have the correct permissions to execute the application: 70 | ```bash 71 | chmod +x AnyLabeling-Folder/anylabeling 72 | ``` 73 | 74 | - If you get dynamic library loading errors, make sure all dependencies are properly installed: 75 | ```bash 76 | pip install -r requirements-macos.txt 77 | ``` 78 | 79 | - If you encounter graphics or UI issues, try using the CPU version instead of GPU. 80 | 81 | - For further assistance, please [open an issue](https://github.com/vietanhdev/anylabeling/issues/new) on GitHub. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | build==1.2.2 3 | twine==6.1.0 4 | 5 | -------------------------------------------------------------------------------- /requirements-gpu-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-gpu.txt 2 | build==1.2.2 3 | twine==6.1.0 4 | -------------------------------------------------------------------------------- /requirements-gpu.txt: -------------------------------------------------------------------------------- 1 | opencv-contrib-python-headless==4.7.0.72 2 | PyQt5==5.15.7 3 | imgviz==1.5.0 4 | natsort==8.1.0 5 | termcolor==1.1.0 6 | PyYAML==6.0.1 7 | onnx==1.16.1 8 | onnxruntime-gpu==1.18.1 9 | qimage2ndarray==1.10.0 10 | darkdetect==0.8.0 11 | -------------------------------------------------------------------------------- /requirements-macos-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements-macos.txt 2 | build==1.2.2 3 | twine==6.1.0 4 | -------------------------------------------------------------------------------- /requirements-macos.txt: -------------------------------------------------------------------------------- 1 | opencv-contrib-python-headless==4.7.0.72 2 | # PyQt5==5.15.6 # Use Miniconda/Anaconda: conda install -c conda-forge pyqt 3 | imgviz==1.5.0 4 | natsort==8.1.0 5 | termcolor==1.1.0 6 | PyYAML==6.0.1 7 | onnx==1.16.1 8 | onnxruntime==1.18.1 9 | qimage2ndarray==1.10.0 10 | darkdetect==0.8.0 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-contrib-python-headless==4.7.0.72 2 | PyQt5==5.15.7 3 | imgviz==1.5.0 4 | natsort==8.1.0 5 | termcolor==1.1.0 6 | PyYAML==6.0.1 7 | onnx==1.16.1 8 | onnxruntime==1.18.1 9 | qimage2ndarray==1.10.0 10 | darkdetect==0.8.0 11 | -------------------------------------------------------------------------------- /sample_images/erol-ahmed-leOh1CzRZVQ-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/erol-ahmed-leOh1CzRZVQ-unsplash.jpg -------------------------------------------------------------------------------- /sample_images/evan-foley-ZgUtMaOVUAY-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/evan-foley-ZgUtMaOVUAY-unsplash.jpg -------------------------------------------------------------------------------- /sample_images/jonas-kakaroto-5JQH9Iqnm9o-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/jonas-kakaroto-5JQH9Iqnm9o-unsplash.jpg -------------------------------------------------------------------------------- /sample_images/julien-goettelmann-nMRE6uR-eP4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/julien-goettelmann-nMRE6uR-eP4-unsplash.jpg -------------------------------------------------------------------------------- /sample_images/national-cancer-institute-L7en7Lb-Ovc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/national-cancer-institute-L7en7Lb-Ovc-unsplash.jpg -------------------------------------------------------------------------------- /sample_images/ryoji-iwata-n31JPLu8_Pw-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vietanhdev/anylabeling/a21f0d6009859733abe06ccf2600b4271e844d75/sample_images/ryoji-iwata-n31JPLu8_Pw-unsplash.jpg -------------------------------------------------------------------------------- /scripts/build_and_publish_pypi.sh: -------------------------------------------------------------------------------- 1 | # ===== Build packages ===== 2 | # For CPU 3 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "CPU"/g' anylabeling/app_info.py 4 | python -m build --no-isolation --outdir wheels_dist 5 | # For GPU 6 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "GPU"/g' anylabeling/app_info.py 7 | python -m build --no-isolation --outdir wheels_dist 8 | # Restore to CPU (default option) 9 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "CPU"/g' anylabeling/app_info.py 10 | 11 | # ===== Publish to PyPi ===== 12 | twine upload --skip-existing wheels_dist/* 13 | -------------------------------------------------------------------------------- /scripts/build_executable.sh: -------------------------------------------------------------------------------- 1 | pyinstaller --noconfirm anylabeling.spec 2 | -------------------------------------------------------------------------------- /scripts/build_macos_folder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to build AnyLabeling in folder mode for macOS 4 | # This creates a directory-based application instead of a bundled .app 5 | 6 | # Set CPU or GPU mode 7 | if [ "$1" == "GPU" ]; then 8 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "GPU"/g' anylabeling/app_info.py 9 | SUFFIX="-GPU" 10 | else 11 | sed -i'' -e 's/\_\_preferred_device\_\_[ ]*=[ ]*\"[A-Za-z0-9]*\"/__preferred_device__ = "CPU"/g' anylabeling/app_info.py 12 | SUFFIX="" 13 | fi 14 | 15 | # Create temporary PyInstaller spec for folder mode 16 | cat > anylabeling_folder.spec << EOL 17 | # -*- mode: python -*- 18 | # vim: ft=python 19 | 20 | import sys 21 | 22 | sys.setrecursionlimit(5000) # required on Windows 23 | 24 | a = Analysis( 25 | ['anylabeling/app.py'], 26 | pathex=['anylabeling'], 27 | binaries=[], 28 | datas=[ 29 | ('anylabeling/configs/auto_labeling/*.yaml', 'anylabeling/configs/auto_labeling'), 30 | ('anylabeling/configs/*.yaml', 'anylabeling/configs'), 31 | ('anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui', 'anylabeling/views/labeling/widgets/auto_labeling') 32 | ], 33 | hiddenimports=[], 34 | hookspath=[], 35 | runtime_hooks=[], 36 | excludes=[], 37 | ) 38 | pyz = PYZ(a.pure, a.zipped_data) 39 | 40 | # Create a directory structure instead of a bundled .app 41 | exe = EXE( 42 | pyz, 43 | a.scripts, 44 | exclude_binaries=True, # This is the key difference - exclude binaries 45 | name='anylabeling', 46 | debug=False, 47 | strip=False, 48 | upx=False, 49 | console=False, 50 | icon='anylabeling/resources/images/icon.icns', 51 | ) 52 | 53 | # Bundle binaries in a separate folder 54 | coll = COLLECT( 55 | exe, 56 | a.binaries, 57 | a.zipfiles, 58 | a.datas, 59 | strip=False, 60 | upx=False, 61 | name='AnyLabeling-Folder${SUFFIX}', 62 | ) 63 | EOL 64 | 65 | # Install PyInstaller if not already installed 66 | pip install pyinstaller 67 | 68 | # Run PyInstaller with the folder mode spec 69 | pyinstaller --noconfirm anylabeling_folder.spec 70 | 71 | # Cleanup 72 | rm anylabeling_folder.spec 73 | 74 | # Print success message 75 | echo "Build completed. Application folder is located at ./dist/AnyLabeling-Folder${SUFFIX}/" 76 | 77 | # Make the script executable 78 | chmod +x scripts/build_macos_folder.sh 79 | -------------------------------------------------------------------------------- /scripts/compile_languages.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | supported_languages = ["en_US", "vi_VN", "zh_CN"] 4 | 5 | for language in supported_languages: 6 | # Compile the .ts file into a .qm file 7 | command = f"lrelease anylabeling/resources/translations/{language}.ts" 8 | os.system(command) 9 | 10 | # Generate resources 11 | command = "pyrcc5 -o anylabeling/resources/resources.py \ 12 | anylabeling/resources/resources.qrc" 13 | os.system(command) 14 | -------------------------------------------------------------------------------- /scripts/generate_languages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | from PyQt5 import QtCore 4 | 5 | supported_languages = ["en_US", "vi_VN", "zh_CN"] 6 | 7 | for language in supported_languages: 8 | # Scan all .py files in the project directory and its subdirectories 9 | py_files = glob.glob(os.path.join("**", "*.py"), recursive=True) 10 | 11 | # Create a QTranslator object to generate the .ts file 12 | translator = QtCore.QTranslator() 13 | 14 | # Translate all .ui files into .py files 15 | ui_files = glob.glob(os.path.join("**", "*.ui"), recursive=True) 16 | for ui_file in ui_files: 17 | py_file = os.path.splitext(ui_file)[0] + "_ui.py" 18 | command = f"pyuic5 -x {ui_file} -o {py_file}" 19 | os.system(command) 20 | 21 | # Extract translations from the .py file 22 | translations_path = "anylabeling/resources/translations" 23 | command = f"pylupdate5 {' '.join(py_files)} -ts {translations_path}/{language}.ts" 24 | os.system(command) 25 | 26 | # Compile the .ts file into a .qm file 27 | command = f"lrelease {translations_path}/{language}.ts" 28 | os.system(command) 29 | 30 | # Generate resources 31 | command = "pyrcc5 -o anylabeling/resources/resources.py \ 32 | anylabeling/resources/resources.qrc" 33 | os.system(command) 34 | -------------------------------------------------------------------------------- /scripts/zip_models.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | import pathlib 4 | import zipfile 5 | from urllib.parse import urlparse 6 | 7 | 8 | output_path = "zipped_models/" 9 | model_config_path = "anylabeling/configs/auto_labeling/" 10 | model_list_path = "anylabeling/configs/auto_labeling/models.yaml" 11 | model_list = yaml.load(open(model_list_path, "r"), Loader=yaml.FullLoader) 12 | 13 | # Create output path 14 | pathlib.Path(output_path).mkdir(parents=True, exist_ok=True) 15 | 16 | 17 | def get_filename_from_url(url): 18 | a = urlparse(url) 19 | return os.path.basename(a.path) 20 | 21 | 22 | for model in model_list: 23 | model_name = model["model_name"] 24 | config_file = model["config_file"] 25 | print(f"Zipping {model_name}...") 26 | 27 | # Get download links 28 | download_links = [] 29 | model_config = yaml.load( 30 | open(model_config_path + config_file, "r"), Loader=yaml.FullLoader 31 | ) 32 | if model_config["type"] == "segment_anything": 33 | download_links.append(model_config["encoder_model_path"]) 34 | download_links.append(model_config["decoder_model_path"]) 35 | else: 36 | download_links.append(model_config["model_path"]) 37 | 38 | model_output_path = os.path.join(output_path, model_name) 39 | pathlib.Path(model_output_path).mkdir(parents=True, exist_ok=True) 40 | 41 | # Save model config 42 | # Rewrite model's urls 43 | if model_config["type"] == "segment_anything": 44 | model_config["encoder_model_path"] = get_filename_from_url( 45 | model_config["encoder_model_path"] 46 | ) 47 | model_config["decoder_model_path"] = get_filename_from_url( 48 | model_config["decoder_model_path"] 49 | ) 50 | else: 51 | model_config["model_path"] = get_filename_from_url(model_config["model_path"]) 52 | with open(os.path.join(model_output_path, "config.yaml"), "w") as f: 53 | yaml.dump(model_config, f) 54 | 55 | # Download models 56 | for link in download_links: 57 | os.system(f"wget -P {model_output_path} {link}") 58 | 59 | # Zip model 60 | with zipfile.ZipFile(os.path.join(output_path, f"{model_name}.zip"), "w") as zip: 61 | for root, _, files in os.walk(model_output_path): 62 | for file in files: 63 | zip.write( 64 | os.path.join(root, file), 65 | os.path.relpath( 66 | os.path.join(root, file), 67 | os.path.join(model_output_path, ".."), 68 | ), 69 | ) 70 | os.system(f"rm -rf {model_output_path}") 71 | 72 | print("Done!") 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import platform 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def get_version(): 8 | """Get package version from app_info.py file""" 9 | filename = "anylabeling/app_info.py" 10 | with open(filename, encoding="utf-8") as f: 11 | match = re.search(r"""^__version__ = ['"]([^'"]*)['"]""", f.read(), re.M) 12 | if not match: 13 | raise RuntimeError(f"{filename} doesn't contain __version__") 14 | version = match.groups()[0] 15 | return version 16 | 17 | 18 | def get_preferred_device(): 19 | """Get preferred device from app_info.py file: CPU or GPU""" 20 | filename = "anylabeling/app_info.py" 21 | with open(filename, encoding="utf-8") as f: 22 | match = re.search( 23 | r"""^__preferred_device__ = ['"]([^'"]*)['"]""", f.read(), re.M 24 | ) 25 | if not match: 26 | raise RuntimeError(f"{filename} doesn't contain __preferred_device__") 27 | device = match.groups()[0] 28 | return device 29 | 30 | 31 | def get_package_name(): 32 | """Get package name based on context""" 33 | package_name = "anylabeling" 34 | preferred_device = get_preferred_device() 35 | if preferred_device == "GPU" and platform.system() != "Darwin": 36 | package_name = "anylabeling-gpu" 37 | return package_name 38 | 39 | 40 | def get_install_requires(): 41 | """Get python requirements based on context""" 42 | install_requires = [ 43 | "imgviz>=0.11", 44 | "natsort>=7.1.0", 45 | "numpy==1.26.4", 46 | "Pillow>=2.8", 47 | "PyYAML==6.0.1", 48 | "termcolor==1.1.0", 49 | "opencv-python-headless==4.7.0.72", 50 | 'PyQt5>=5.15.7; platform_system != "Darwin"', 51 | "onnx==1.16.1", 52 | "qimage2ndarray==1.10.0", 53 | "darkdetect==0.8.0", 54 | ] 55 | 56 | # Add onnxruntime-gpu if GPU is preferred 57 | # otherwise, add onnxruntime. 58 | # Note: onnxruntime-gpu is not available on macOS 59 | preferred_device = get_preferred_device() 60 | if preferred_device == "GPU" and platform.system() != "Darwin": 61 | install_requires.append("onnxruntime-gpu==1.18.1") 62 | print("Building AnyLabeling with GPU support") 63 | else: 64 | install_requires.append("onnxruntime==1.18.1") 65 | print("Building AnyLabeling without GPU support") 66 | 67 | return install_requires 68 | 69 | 70 | def get_long_description(): 71 | """Read long description from README""" 72 | with open("README.md", encoding="utf-8") as f: 73 | long_description = f.read() 74 | return long_description 75 | 76 | 77 | setup( 78 | name=get_package_name(), 79 | version=get_version(), 80 | packages=find_packages(), 81 | description="Effortless data labeling with AI support", 82 | long_description=get_long_description(), 83 | long_description_content_type="text/markdown", 84 | author="Viet-Anh Nguyen", 85 | author_email="vietanh.dev@gmail.com", 86 | url="https://github.com/vietanhdev/anylabeling", 87 | install_requires=get_install_requires(), 88 | license="GPLv3", 89 | keywords="Image Annotation, Machine Learning, Deep Learning", 90 | classifiers=[ 91 | "Intended Audience :: Developers", 92 | "Intended Audience :: Science/Research", 93 | "Natural Language :: English", 94 | "Operating System :: OS Independent", 95 | "Programming Language :: Python", 96 | "Programming Language :: Python :: 3.10", 97 | "Programming Language :: Python :: 3.11", 98 | "Programming Language :: Python :: 3.12", 99 | "Programming Language :: Python :: 3 :: Only", 100 | ], 101 | include_package_data=True, 102 | entry_points={ 103 | "console_scripts": [ 104 | "anylabeling=anylabeling.app:main", 105 | ], 106 | }, 107 | ) 108 | --------------------------------------------------------------------------------