├── .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 |
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 | 
9 |
10 | [](https://pypi.org/project/anylabeling)
11 | [](https://github.com/vietanhdev/anylabeling/blob/master/LICENSE)
12 | [](https://github.com/vietanhdev/anylabeling/issues)
13 | [](https://pypi.org/project/anylabeling/)
14 | [](https://anylabeling.nrl.ai/)
15 | []([[https://anylabeling.nrl.ai/](https://twitter.com/vietanhdev)](https://twitter.com/vietanhdev))
16 |
17 | [](https://anylearning.nrl.ai/)
18 |
19 | [](https://anylearning.nrl.ai/)
20 |
21 |
22 |
23 |
24 |
25 |
26 | **Auto Labeling with Segment Anything**
27 |
28 |
29 |
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 | [](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 |
--------------------------------------------------------------------------------
/anylabeling/resources/icons/tools.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------