lazylabel-gui 1.1.1__tar.gz → 1.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lazylabel_gui-1.1.1/src/lazylabel_gui.egg-info → lazylabel_gui-1.1.3}/PKG-INFO +48 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/README.md +42 -2
- lazylabel_gui-1.1.3/pyproject.toml +93 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/__init__.py +1 -1
- lazylabel_gui-1.1.3/src/lazylabel/config/__init__.py +7 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/config/hotkeys.py +96 -58
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/config/paths.py +8 -9
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/config/settings.py +15 -16
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/core/__init__.py +3 -3
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/core/file_manager.py +49 -33
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/core/model_manager.py +9 -11
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/core/segment_manager.py +21 -22
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/main.py +1 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/models/__init__.py +1 -1
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/models/sam_model.py +24 -19
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/__init__.py +3 -3
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/control_panel.py +21 -19
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/editable_vertex.py +16 -3
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/hotkey_dialog.py +125 -93
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/hoverable_polygon_item.py +1 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/main_window.py +290 -49
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/photo_viewer.py +4 -7
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/reorderable_class_table.py +2 -3
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/right_panel.py +15 -16
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/widgets/__init__.py +1 -1
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/widgets/adjustments_widget.py +22 -21
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/widgets/model_selection_widget.py +28 -21
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/widgets/settings_widget.py +35 -28
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/widgets/status_bar.py +2 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/utils/__init__.py +2 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/utils/custom_file_system_model.py +3 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3/src/lazylabel_gui.egg-info}/PKG-INFO +48 -2
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel_gui.egg-info/requires.txt +7 -0
- lazylabel_gui-1.1.1/pyproject.toml +0 -40
- lazylabel_gui-1.1.1/src/lazylabel/config/__init__.py +0 -7
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/LICENSE +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/setup.cfg +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/hoverable_pixelmap_item.py +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/ui/numeric_table_widget_item.py +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel/utils/utils.py +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
- {lazylabel_gui-1.1.1 → lazylabel_gui-1.1.3}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lazylabel-gui
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.3
|
4
4
|
Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
|
5
5
|
Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
|
6
6
|
License: MIT License
|
@@ -45,6 +45,12 @@ Requires-Dist: opencv-python>=4.11.0.86
|
|
45
45
|
Requires-Dist: scipy>=1.15.3
|
46
46
|
Requires-Dist: requests>=2.32.4
|
47
47
|
Requires-Dist: tqdm>=4.67.1
|
48
|
+
Provides-Extra: dev
|
49
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
50
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
51
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
52
|
+
Requires-Dist: pytest-qt>=4.2.0; extra == "dev"
|
53
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
48
54
|
Dynamic: license-file
|
49
55
|
|
50
56
|
# <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo2.png" alt="LazyLabel Logo" style="height:60px; vertical-align:middle;" /> <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo_black.png" alt="LazyLabel Cursive" style="height:60px; vertical-align:middle;" />
|
@@ -142,7 +148,8 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
|
|
142
148
|
|---|---|
|
143
149
|
| `L-Click` | Add positive point (Point Mode) or polygon vertex. |
|
144
150
|
| `R-Click` | Add negative point (Point Mode). |
|
145
|
-
| `Ctrl + Z` | Undo last
|
151
|
+
| `Ctrl + Z` | Undo last action. |
|
152
|
+
| `Ctrl + Y` / `Ctrl + Shift + Z` | Redo last action. |
|
146
153
|
| `Spacebar` | Finalize and save current AI segment. |
|
147
154
|
| `Enter` | **Save final mask for the current image to a `.npz` file.** |
|
148
155
|
| `M` | **Merge** selected segments into a single class. |
|
@@ -191,6 +198,45 @@ LazyLabel includes a comprehensive hotkey management system:
|
|
191
198
|
|
192
199
|
For complete hotkey documentation, see [HOTKEY_FEATURE.md](src/lazylabel/HOTKEY_FEATURE.md).
|
193
200
|
|
201
|
+
## Development
|
202
|
+
|
203
|
+
### Code Quality
|
204
|
+
This project uses Ruff for linting and formatting:
|
205
|
+
|
206
|
+
```bash
|
207
|
+
# Activate virtual environment first
|
208
|
+
& e:\venv\lazylabel\Scripts\Activate.ps1
|
209
|
+
|
210
|
+
# Run linter
|
211
|
+
ruff check .
|
212
|
+
|
213
|
+
# Fix auto-fixable issues
|
214
|
+
ruff check --fix .
|
215
|
+
|
216
|
+
# Format code
|
217
|
+
ruff format .
|
218
|
+
|
219
|
+
# Check if code is properly formatted
|
220
|
+
ruff format --check .
|
221
|
+
```
|
222
|
+
|
223
|
+
### Testing
|
224
|
+
|
225
|
+
Run tests using pytest:
|
226
|
+
|
227
|
+
```bash
|
228
|
+
# Run all tests
|
229
|
+
python -m pytest
|
230
|
+
|
231
|
+
# Run tests with coverage
|
232
|
+
python -m pytest --cov=lazylabel --cov-report=html --cov-report=term-missing
|
233
|
+
|
234
|
+
# Run specific test file
|
235
|
+
python -m pytest tests/unit/ui/test_undo_redo.py -v
|
236
|
+
```
|
237
|
+
|
238
|
+
The HTML coverage report will be generated in `htmlcov/` directory.
|
239
|
+
|
194
240
|
---
|
195
241
|
|
196
242
|
## ☕ Support LazyLabel
|
@@ -93,7 +93,8 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
|
|
93
93
|
|---|---|
|
94
94
|
| `L-Click` | Add positive point (Point Mode) or polygon vertex. |
|
95
95
|
| `R-Click` | Add negative point (Point Mode). |
|
96
|
-
| `Ctrl + Z` | Undo last
|
96
|
+
| `Ctrl + Z` | Undo last action. |
|
97
|
+
| `Ctrl + Y` / `Ctrl + Shift + Z` | Redo last action. |
|
97
98
|
| `Spacebar` | Finalize and save current AI segment. |
|
98
99
|
| `Enter` | **Save final mask for the current image to a `.npz` file.** |
|
99
100
|
| `M` | **Merge** selected segments into a single class. |
|
@@ -142,7 +143,46 @@ LazyLabel includes a comprehensive hotkey management system:
|
|
142
143
|
|
143
144
|
For complete hotkey documentation, see [HOTKEY_FEATURE.md](src/lazylabel/HOTKEY_FEATURE.md).
|
144
145
|
|
146
|
+
## Development
|
147
|
+
|
148
|
+
### Code Quality
|
149
|
+
This project uses Ruff for linting and formatting:
|
150
|
+
|
151
|
+
```bash
|
152
|
+
# Activate virtual environment first
|
153
|
+
& e:\venv\lazylabel\Scripts\Activate.ps1
|
154
|
+
|
155
|
+
# Run linter
|
156
|
+
ruff check .
|
157
|
+
|
158
|
+
# Fix auto-fixable issues
|
159
|
+
ruff check --fix .
|
160
|
+
|
161
|
+
# Format code
|
162
|
+
ruff format .
|
163
|
+
|
164
|
+
# Check if code is properly formatted
|
165
|
+
ruff format --check .
|
166
|
+
```
|
167
|
+
|
168
|
+
### Testing
|
169
|
+
|
170
|
+
Run tests using pytest:
|
171
|
+
|
172
|
+
```bash
|
173
|
+
# Run all tests
|
174
|
+
python -m pytest
|
175
|
+
|
176
|
+
# Run tests with coverage
|
177
|
+
python -m pytest --cov=lazylabel --cov-report=html --cov-report=term-missing
|
178
|
+
|
179
|
+
# Run specific test file
|
180
|
+
python -m pytest tests/unit/ui/test_undo_redo.py -v
|
181
|
+
```
|
182
|
+
|
183
|
+
The HTML coverage report will be generated in `htmlcov/` directory.
|
184
|
+
|
145
185
|
---
|
146
186
|
|
147
187
|
## ☕ Support LazyLabel
|
148
|
-
[If you found LazyLabel helpful, consider supporting the project!](https://buymeacoffee.com/dnzckn)
|
188
|
+
[If you found LazyLabel helpful, consider supporting the project!](https://buymeacoffee.com/dnzckn)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=61.0"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "lazylabel-gui"
|
7
|
+
version = "1.1.3"
|
8
|
+
authors = [
|
9
|
+
{ name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
|
10
|
+
]
|
11
|
+
description = "An image segmentation GUI for generating ML ready mask tensors and annotations."
|
12
|
+
readme = "README.md"
|
13
|
+
license = { file="LICENSE" }
|
14
|
+
requires-python = ">=3.10"
|
15
|
+
classifiers = [
|
16
|
+
"Programming Language :: Python :: 3",
|
17
|
+
"License :: OSI Approved :: MIT License",
|
18
|
+
"Operating System :: OS Independent",
|
19
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
20
|
+
"Environment :: X11 Applications :: Qt",
|
21
|
+
]
|
22
|
+
dependencies = [
|
23
|
+
"PyQt6>=6.9.0",
|
24
|
+
"pyqtdarktheme==2.1.0",
|
25
|
+
"torch>=2.7.1",
|
26
|
+
"torchvision>=0.22.1",
|
27
|
+
"segment-anything==1.0",
|
28
|
+
"numpy>=2.1.2",
|
29
|
+
"opencv-python>=4.11.0.86",
|
30
|
+
"scipy>=1.15.3",
|
31
|
+
"requests>=2.32.4",
|
32
|
+
"tqdm>=4.67.1"
|
33
|
+
]
|
34
|
+
|
35
|
+
[project.urls]
|
36
|
+
"Homepage" = "https://github.com/dnzckn/lazylabel"
|
37
|
+
"Bug Tracker" = "https://github.com/dnzckn/lazylabel/issues"
|
38
|
+
|
39
|
+
[project.scripts]
|
40
|
+
lazylabel-gui = "lazylabel.main:main"
|
41
|
+
|
42
|
+
[project.optional-dependencies]
|
43
|
+
dev = [
|
44
|
+
"pytest>=7.0.0",
|
45
|
+
"pytest-cov>=4.0.0",
|
46
|
+
"pytest-mock>=3.10.0",
|
47
|
+
"pytest-qt>=4.2.0",
|
48
|
+
"ruff>=0.8.0",
|
49
|
+
]
|
50
|
+
|
51
|
+
[tool.pytest.ini_options]
|
52
|
+
testpaths = ["tests"]
|
53
|
+
python_files = "test_*.py"
|
54
|
+
python_classes = "Test*"
|
55
|
+
python_functions = "test_*"
|
56
|
+
|
57
|
+
[tool.ruff]
|
58
|
+
target-version = "py310"
|
59
|
+
line-length = 88
|
60
|
+
exclude = [
|
61
|
+
".git",
|
62
|
+
".pytest_cache",
|
63
|
+
"__pycache__",
|
64
|
+
"dist",
|
65
|
+
"build",
|
66
|
+
"*.egg-info",
|
67
|
+
]
|
68
|
+
|
69
|
+
[tool.ruff.lint]
|
70
|
+
select = [
|
71
|
+
"E", # pycodestyle errors
|
72
|
+
"W", # pycodestyle warnings
|
73
|
+
"F", # pyflakes
|
74
|
+
"I", # isort
|
75
|
+
"B", # flake8-bugbear
|
76
|
+
"C4", # flake8-comprehensions
|
77
|
+
"UP", # pyupgrade
|
78
|
+
"SIM", # flake8-simplify
|
79
|
+
]
|
80
|
+
ignore = [
|
81
|
+
"E501", # line too long, handled by formatter
|
82
|
+
"B008", # do not perform function calls in argument defaults
|
83
|
+
"C901", # too complex
|
84
|
+
]
|
85
|
+
|
86
|
+
[tool.ruff.lint.per-file-ignores]
|
87
|
+
"tests/**/*" = ["B011", "S101"] # Allow assert statements in tests
|
88
|
+
|
89
|
+
[tool.ruff.format]
|
90
|
+
quote-style = "double"
|
91
|
+
indent-style = "space"
|
92
|
+
skip-magic-trailing-comma = false
|
93
|
+
line-ending = "auto"
|
@@ -2,89 +2,127 @@
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
5
|
-
from dataclasses import dataclass
|
6
|
-
|
7
|
-
from PyQt6.QtCore import Qt
|
5
|
+
from dataclasses import dataclass
|
6
|
+
|
8
7
|
from PyQt6.QtGui import QKeySequence
|
9
8
|
|
10
9
|
|
11
10
|
@dataclass
|
12
11
|
class HotkeyAction:
|
13
12
|
"""Represents a hotkey action with primary and secondary keys."""
|
13
|
+
|
14
14
|
name: str
|
15
15
|
description: str
|
16
16
|
primary_key: str
|
17
|
-
secondary_key:
|
17
|
+
secondary_key: str | None = None
|
18
18
|
category: str = "General"
|
19
19
|
mouse_related: bool = False # Cannot be reassigned if True
|
20
20
|
|
21
21
|
|
22
22
|
class HotkeyManager:
|
23
23
|
"""Manages application hotkeys with persistence."""
|
24
|
-
|
24
|
+
|
25
25
|
def __init__(self, config_dir: str):
|
26
26
|
self.config_dir = config_dir
|
27
27
|
self.hotkeys_file = os.path.join(config_dir, "hotkeys.json")
|
28
|
-
self.actions:
|
28
|
+
self.actions: dict[str, HotkeyAction] = {}
|
29
29
|
self._initialize_default_hotkeys()
|
30
30
|
self.load_hotkeys()
|
31
|
-
|
31
|
+
|
32
32
|
def _initialize_default_hotkeys(self):
|
33
33
|
"""Initialize default hotkey mappings."""
|
34
34
|
default_hotkeys = [
|
35
35
|
# Navigation
|
36
|
-
HotkeyAction(
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
HotkeyAction(
|
37
|
+
"load_next_image", "Load Next Image", "Right", category="Navigation"
|
38
|
+
),
|
39
|
+
HotkeyAction(
|
40
|
+
"load_previous_image",
|
41
|
+
"Load Previous Image",
|
42
|
+
"Left",
|
43
|
+
category="Navigation",
|
44
|
+
),
|
45
|
+
HotkeyAction("fit_view", "Fit View", ".", category="Navigation"),
|
40
46
|
# Modes
|
41
47
|
HotkeyAction("sam_mode", "Point Mode (SAM)", "1", category="Modes"),
|
42
48
|
HotkeyAction("polygon_mode", "Polygon Mode", "2", category="Modes"),
|
43
49
|
HotkeyAction("selection_mode", "Selection Mode", "E", category="Modes"),
|
44
50
|
HotkeyAction("pan_mode", "Pan Mode", "Q", category="Modes"),
|
45
51
|
HotkeyAction("edit_mode", "Edit Mode", "R", category="Modes"),
|
46
|
-
|
47
52
|
# Actions
|
48
|
-
HotkeyAction(
|
49
|
-
|
53
|
+
HotkeyAction(
|
54
|
+
"clear_points", "Clear Points/Vertices", "C", category="Actions"
|
55
|
+
),
|
56
|
+
HotkeyAction(
|
57
|
+
"save_segment", "Save Current Segment", "Space", category="Actions"
|
58
|
+
),
|
50
59
|
HotkeyAction("save_output", "Save Output", "Return", category="Actions"),
|
51
|
-
HotkeyAction(
|
60
|
+
HotkeyAction(
|
61
|
+
"save_output_alt", "Save Output (Alt)", "Enter", category="Actions"
|
62
|
+
),
|
52
63
|
HotkeyAction("undo", "Undo Last Action", "Ctrl+Z", category="Actions"),
|
53
|
-
HotkeyAction(
|
54
|
-
|
64
|
+
HotkeyAction(
|
65
|
+
"redo", "Redo Last Action", "Ctrl+Y", "Ctrl+Shift+Z", category="Actions"
|
66
|
+
),
|
67
|
+
HotkeyAction(
|
68
|
+
"escape", "Cancel/Clear Selection", "Escape", category="Actions"
|
69
|
+
),
|
55
70
|
# Segments
|
56
|
-
HotkeyAction(
|
57
|
-
|
58
|
-
|
59
|
-
HotkeyAction(
|
60
|
-
|
71
|
+
HotkeyAction(
|
72
|
+
"merge_segments", "Merge Selected Segments", "M", category="Segments"
|
73
|
+
),
|
74
|
+
HotkeyAction(
|
75
|
+
"delete_segments", "Delete Selected Segments", "V", category="Segments"
|
76
|
+
),
|
77
|
+
HotkeyAction(
|
78
|
+
"delete_segments_alt",
|
79
|
+
"Delete Selected Segments (Alt)",
|
80
|
+
"Backspace",
|
81
|
+
category="Segments",
|
82
|
+
),
|
83
|
+
HotkeyAction(
|
84
|
+
"select_all", "Select All Segments", "Ctrl+A", category="Segments"
|
85
|
+
),
|
61
86
|
# View
|
62
87
|
HotkeyAction("zoom_in", "Zoom In", "Ctrl+Plus", category="View"),
|
63
88
|
HotkeyAction("zoom_out", "Zoom Out", "Ctrl+Minus", category="View"),
|
64
|
-
|
65
89
|
# Movement (WASD)
|
66
90
|
HotkeyAction("pan_up", "Pan Up", "W", category="Movement"),
|
67
91
|
HotkeyAction("pan_down", "Pan Down", "S", category="Movement"),
|
68
92
|
HotkeyAction("pan_left", "Pan Left", "A", category="Movement"),
|
69
93
|
HotkeyAction("pan_right", "Pan Right", "D", category="Movement"),
|
70
|
-
|
71
94
|
# Mouse-related (cannot be reassigned)
|
72
|
-
HotkeyAction(
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
95
|
+
HotkeyAction(
|
96
|
+
"left_click",
|
97
|
+
"Add Positive Point / Select",
|
98
|
+
"Left Click",
|
99
|
+
category="Mouse",
|
100
|
+
mouse_related=True,
|
101
|
+
),
|
102
|
+
HotkeyAction(
|
103
|
+
"right_click",
|
104
|
+
"Add Negative Point",
|
105
|
+
"Right Click",
|
106
|
+
category="Mouse",
|
107
|
+
mouse_related=True,
|
108
|
+
),
|
109
|
+
HotkeyAction(
|
110
|
+
"mouse_drag",
|
111
|
+
"Drag/Pan",
|
112
|
+
"Mouse Drag",
|
113
|
+
category="Mouse",
|
114
|
+
mouse_related=True,
|
115
|
+
),
|
78
116
|
]
|
79
|
-
|
117
|
+
|
80
118
|
for action in default_hotkeys:
|
81
119
|
self.actions[action.name] = action
|
82
|
-
|
83
|
-
def get_action(self, action_name: str) ->
|
120
|
+
|
121
|
+
def get_action(self, action_name: str) -> HotkeyAction | None:
|
84
122
|
"""Get hotkey action by name."""
|
85
123
|
return self.actions.get(action_name)
|
86
|
-
|
87
|
-
def get_actions_by_category(self) ->
|
124
|
+
|
125
|
+
def get_actions_by_category(self) -> dict[str, list[HotkeyAction]]:
|
88
126
|
"""Get actions grouped by category."""
|
89
127
|
categories = {}
|
90
128
|
for action in self.actions.values():
|
@@ -92,29 +130,29 @@ class HotkeyManager:
|
|
92
130
|
categories[action.category] = []
|
93
131
|
categories[action.category].append(action)
|
94
132
|
return categories
|
95
|
-
|
133
|
+
|
96
134
|
def set_primary_key(self, action_name: str, key: str) -> bool:
|
97
135
|
"""Set primary key for an action."""
|
98
136
|
if action_name in self.actions and not self.actions[action_name].mouse_related:
|
99
137
|
self.actions[action_name].primary_key = key
|
100
138
|
return True
|
101
139
|
return False
|
102
|
-
|
103
|
-
def set_secondary_key(self, action_name: str, key:
|
140
|
+
|
141
|
+
def set_secondary_key(self, action_name: str, key: str | None) -> bool:
|
104
142
|
"""Set secondary key for an action."""
|
105
143
|
if action_name in self.actions and not self.actions[action_name].mouse_related:
|
106
144
|
self.actions[action_name].secondary_key = key
|
107
145
|
return True
|
108
146
|
return False
|
109
|
-
|
110
|
-
def get_key_for_action(self, action_name: str) ->
|
147
|
+
|
148
|
+
def get_key_for_action(self, action_name: str) -> tuple[str | None, str | None]:
|
111
149
|
"""Get primary and secondary keys for an action."""
|
112
150
|
action = self.actions.get(action_name)
|
113
151
|
if action:
|
114
152
|
return action.primary_key, action.secondary_key
|
115
153
|
return None, None
|
116
|
-
|
117
|
-
def is_key_in_use(self, key: str, exclude_action: str = None) ->
|
154
|
+
|
155
|
+
def is_key_in_use(self, key: str, exclude_action: str = None) -> str | None:
|
118
156
|
"""Check if a key is already in use by another action."""
|
119
157
|
for name, action in self.actions.items():
|
120
158
|
if name == exclude_action:
|
@@ -122,48 +160,48 @@ class HotkeyManager:
|
|
122
160
|
if action.primary_key == key or action.secondary_key == key:
|
123
161
|
return name
|
124
162
|
return None
|
125
|
-
|
163
|
+
|
126
164
|
def reset_to_defaults(self):
|
127
165
|
"""Reset all hotkeys to default values."""
|
128
166
|
self._initialize_default_hotkeys()
|
129
|
-
|
167
|
+
|
130
168
|
def save_hotkeys(self):
|
131
169
|
"""Save hotkeys to file."""
|
132
170
|
os.makedirs(self.config_dir, exist_ok=True)
|
133
|
-
|
171
|
+
|
134
172
|
# Convert to serializable format
|
135
173
|
data = {}
|
136
174
|
for name, action in self.actions.items():
|
137
175
|
if not action.mouse_related: # Don't save mouse-related actions
|
138
176
|
data[name] = {
|
139
|
-
|
140
|
-
|
177
|
+
"primary_key": action.primary_key,
|
178
|
+
"secondary_key": action.secondary_key,
|
141
179
|
}
|
142
|
-
|
143
|
-
with open(self.hotkeys_file,
|
180
|
+
|
181
|
+
with open(self.hotkeys_file, "w") as f:
|
144
182
|
json.dump(data, f, indent=4)
|
145
|
-
|
183
|
+
|
146
184
|
def load_hotkeys(self):
|
147
185
|
"""Load hotkeys from file."""
|
148
186
|
if not os.path.exists(self.hotkeys_file):
|
149
187
|
return
|
150
|
-
|
188
|
+
|
151
189
|
try:
|
152
|
-
with open(self.hotkeys_file
|
190
|
+
with open(self.hotkeys_file) as f:
|
153
191
|
data = json.load(f)
|
154
|
-
|
192
|
+
|
155
193
|
for name, keys in data.items():
|
156
194
|
if name in self.actions and not self.actions[name].mouse_related:
|
157
|
-
self.actions[name].primary_key = keys.get(
|
158
|
-
self.actions[name].secondary_key = keys.get(
|
195
|
+
self.actions[name].primary_key = keys.get("primary_key", "")
|
196
|
+
self.actions[name].secondary_key = keys.get("secondary_key")
|
159
197
|
except (json.JSONDecodeError, KeyError, FileNotFoundError):
|
160
198
|
# If loading fails, keep defaults
|
161
199
|
pass
|
162
|
-
|
200
|
+
|
163
201
|
def key_sequence_to_string(self, key_sequence: QKeySequence) -> str:
|
164
202
|
"""Convert QKeySequence to string representation."""
|
165
203
|
return key_sequence.toString()
|
166
|
-
|
204
|
+
|
167
205
|
def string_to_key_sequence(self, key_string: str) -> QKeySequence:
|
168
206
|
"""Convert string to QKeySequence."""
|
169
|
-
return QKeySequence(key_string)
|
207
|
+
return QKeySequence(key_string)
|
@@ -1,41 +1,40 @@
|
|
1
1
|
"""Path management for LazyLabel."""
|
2
2
|
|
3
|
-
import os
|
4
3
|
from pathlib import Path
|
5
4
|
|
6
5
|
|
7
6
|
class Paths:
|
8
7
|
"""Centralized path management."""
|
9
|
-
|
8
|
+
|
10
9
|
def __init__(self):
|
11
10
|
self.app_dir = Path(__file__).parent.parent
|
12
11
|
self.models_dir = self.app_dir / "models"
|
13
12
|
self.config_dir = Path.home() / ".config" / "lazylabel"
|
14
13
|
self.cache_dir = Path.home() / ".cache" / "lazylabel"
|
15
|
-
|
14
|
+
|
16
15
|
# Ensure directories exist
|
17
16
|
self.models_dir.mkdir(exist_ok=True)
|
18
17
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
19
|
-
|
18
|
+
|
20
19
|
@property
|
21
20
|
def settings_file(self) -> Path:
|
22
21
|
"""Path to settings file."""
|
23
22
|
return self.config_dir / "settings.json"
|
24
|
-
|
23
|
+
|
25
24
|
@property
|
26
25
|
def demo_pictures_dir(self) -> Path:
|
27
26
|
"""Path to demo pictures directory."""
|
28
27
|
return self.app_dir / "demo_pictures"
|
29
|
-
|
28
|
+
|
30
29
|
@property
|
31
30
|
def logo_path(self) -> Path:
|
32
31
|
"""Path to application logo."""
|
33
32
|
return self.demo_pictures_dir / "logo2.png"
|
34
|
-
|
33
|
+
|
35
34
|
def get_model_path(self, filename: str) -> Path:
|
36
35
|
"""Get path for a model file."""
|
37
36
|
return self.models_dir / filename
|
38
|
-
|
37
|
+
|
39
38
|
def get_old_cache_model_path(self, filename: str) -> Path:
|
40
39
|
"""Get path for model in old cache location."""
|
41
|
-
return self.cache_dir / filename
|
40
|
+
return self.cache_dir / filename
|
@@ -1,60 +1,59 @@
|
|
1
1
|
"""Application settings and configuration."""
|
2
2
|
|
3
|
-
import os
|
4
|
-
from dataclasses import dataclass, asdict
|
5
|
-
from typing import Dict, Any
|
6
3
|
import json
|
4
|
+
import os
|
5
|
+
from dataclasses import asdict, dataclass
|
7
6
|
|
8
7
|
|
9
8
|
@dataclass
|
10
9
|
class Settings:
|
11
10
|
"""Application settings with defaults."""
|
12
|
-
|
11
|
+
|
13
12
|
# UI Settings
|
14
13
|
window_width: int = 1600
|
15
14
|
window_height: int = 900
|
16
15
|
left_panel_width: int = 250
|
17
16
|
right_panel_width: int = 350
|
18
|
-
|
17
|
+
|
19
18
|
# Annotation Settings
|
20
19
|
point_radius: float = 0.3
|
21
20
|
line_thickness: float = 0.5
|
22
21
|
pan_multiplier: float = 1.0
|
23
22
|
polygon_join_threshold: int = 2
|
24
|
-
|
23
|
+
|
25
24
|
# Model Settings
|
26
25
|
default_model_type: str = "vit_h"
|
27
26
|
default_model_filename: str = "sam_vit_h_4b8939.pth"
|
28
|
-
|
27
|
+
|
29
28
|
# Save Settings
|
30
29
|
auto_save: bool = True
|
31
30
|
save_npz: bool = True
|
32
31
|
save_txt: bool = True
|
33
32
|
save_class_aliases: bool = False
|
34
33
|
yolo_use_alias: bool = True
|
35
|
-
|
34
|
+
|
36
35
|
# UI State
|
37
36
|
annotation_size_multiplier: float = 1.0
|
38
|
-
|
37
|
+
|
39
38
|
def save_to_file(self, filepath: str) -> None:
|
40
39
|
"""Save settings to JSON file."""
|
41
40
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
42
|
-
with open(filepath,
|
41
|
+
with open(filepath, "w") as f:
|
43
42
|
json.dump(asdict(self), f, indent=4)
|
44
|
-
|
43
|
+
|
45
44
|
@classmethod
|
46
|
-
def load_from_file(cls, filepath: str) ->
|
45
|
+
def load_from_file(cls, filepath: str) -> "Settings":
|
47
46
|
"""Load settings from JSON file."""
|
48
47
|
if not os.path.exists(filepath):
|
49
48
|
return cls()
|
50
|
-
|
49
|
+
|
51
50
|
try:
|
52
|
-
with open(filepath
|
51
|
+
with open(filepath) as f:
|
53
52
|
data = json.load(f)
|
54
53
|
return cls(**data)
|
55
54
|
except (json.JSONDecodeError, TypeError):
|
56
55
|
return cls()
|
57
|
-
|
56
|
+
|
58
57
|
def update(self, **kwargs) -> None:
|
59
58
|
"""Update settings with new values."""
|
60
59
|
for key, value in kwargs.items():
|
@@ -63,4 +62,4 @@ class Settings:
|
|
63
62
|
|
64
63
|
|
65
64
|
# Default settings instance
|
66
|
-
DEFAULT_SETTINGS = Settings()
|
65
|
+
DEFAULT_SETTINGS = Settings()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Core business logic for LazyLabel."""
|
2
2
|
|
3
|
-
from .segment_manager import SegmentManager
|
4
|
-
from .model_manager import ModelManager
|
5
3
|
from .file_manager import FileManager
|
4
|
+
from .model_manager import ModelManager
|
5
|
+
from .segment_manager import SegmentManager
|
6
6
|
|
7
|
-
__all__ = [
|
7
|
+
__all__ = ["SegmentManager", "ModelManager", "FileManager"]
|