lazylabel-gui 1.1.2__py3-none-any.whl → 1.1.4__py3-none-any.whl
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/__init__.py +9 -9
- lazylabel/config/__init__.py +7 -7
- lazylabel/config/hotkeys.py +96 -57
- lazylabel/config/paths.py +40 -41
- lazylabel/config/settings.py +65 -66
- lazylabel/core/__init__.py +7 -7
- lazylabel/core/file_manager.py +122 -106
- lazylabel/core/model_manager.py +95 -97
- lazylabel/core/segment_manager.py +183 -171
- lazylabel/main.py +37 -36
- lazylabel/models/__init__.py +5 -5
- lazylabel/models/sam_model.py +200 -195
- lazylabel/ui/__init__.py +8 -8
- lazylabel/ui/control_panel.py +245 -241
- lazylabel/ui/editable_vertex.py +64 -64
- lazylabel/ui/hotkey_dialog.py +416 -384
- lazylabel/ui/hoverable_pixelmap_item.py +22 -22
- lazylabel/ui/hoverable_polygon_item.py +38 -39
- lazylabel/ui/main_window.py +1860 -1659
- lazylabel/ui/numeric_table_widget_item.py +9 -9
- lazylabel/ui/photo_viewer.py +51 -54
- lazylabel/ui/reorderable_class_table.py +60 -61
- lazylabel/ui/right_panel.py +332 -315
- lazylabel/ui/widgets/__init__.py +8 -8
- lazylabel/ui/widgets/adjustments_widget.py +108 -108
- lazylabel/ui/widgets/model_selection_widget.py +101 -94
- lazylabel/ui/widgets/settings_widget.py +113 -106
- lazylabel/ui/widgets/status_bar.py +109 -109
- lazylabel/utils/__init__.py +6 -6
- lazylabel/utils/custom_file_system_model.py +133 -132
- lazylabel/utils/utils.py +12 -12
- {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/METADATA +243 -197
- lazylabel_gui-1.1.4.dist-info/RECORD +37 -0
- {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/licenses/LICENSE +21 -21
- lazylabel_gui-1.1.2.dist-info/RECORD +0 -37
- {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.2.dist-info → lazylabel_gui-1.1.4.dist-info}/top_level.txt +0 -0
lazylabel/__init__.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
"""LazyLabel - AI-assisted image segmentation tool."""
|
2
|
-
|
3
|
-
__version__ = "1.0.9"
|
4
|
-
__author__ = "Deniz N. Cakan"
|
5
|
-
__email__ = "deniz.n.cakan@gmail.com"
|
6
|
-
|
7
|
-
from .main import main
|
8
|
-
|
9
|
-
__all__ = [
|
1
|
+
"""LazyLabel - AI-assisted image segmentation tool."""
|
2
|
+
|
3
|
+
__version__ = "1.0.9"
|
4
|
+
__author__ = "Deniz N. Cakan"
|
5
|
+
__email__ = "deniz.n.cakan@gmail.com"
|
6
|
+
|
7
|
+
from .main import main
|
8
|
+
|
9
|
+
__all__ = ["main"]
|
lazylabel/config/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
"""Configuration management for LazyLabel."""
|
2
|
-
|
3
|
-
from .
|
4
|
-
from .paths import Paths
|
5
|
-
from .
|
6
|
-
|
7
|
-
__all__ = [
|
1
|
+
"""Configuration management for LazyLabel."""
|
2
|
+
|
3
|
+
from .hotkeys import HotkeyAction, HotkeyManager
|
4
|
+
from .paths import Paths
|
5
|
+
from .settings import DEFAULT_SETTINGS, Settings
|
6
|
+
|
7
|
+
__all__ = ["Settings", "DEFAULT_SETTINGS", "Paths", "HotkeyManager", "HotkeyAction"]
|
lazylabel/config/hotkeys.py
CHANGED
@@ -2,89 +2,128 @@
|
|
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
|
-
|
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
|
+
),
|
38
45
|
HotkeyAction("fit_view", "Fit View", ".", category="Navigation"),
|
39
|
-
|
40
46
|
# Modes
|
41
47
|
HotkeyAction("sam_mode", "Point Mode (SAM)", "1", category="Modes"),
|
42
48
|
HotkeyAction("polygon_mode", "Polygon Mode", "2", category="Modes"),
|
49
|
+
HotkeyAction("bbox_mode", "Bounding Box Mode", "3", category="Modes"),
|
43
50
|
HotkeyAction("selection_mode", "Selection Mode", "E", category="Modes"),
|
44
51
|
HotkeyAction("pan_mode", "Pan Mode", "Q", category="Modes"),
|
45
52
|
HotkeyAction("edit_mode", "Edit Mode", "R", category="Modes"),
|
46
|
-
|
47
53
|
# Actions
|
48
|
-
HotkeyAction(
|
49
|
-
|
54
|
+
HotkeyAction(
|
55
|
+
"clear_points", "Clear Points/Vertices", "C", category="Actions"
|
56
|
+
),
|
57
|
+
HotkeyAction(
|
58
|
+
"save_segment", "Save Current Segment", "Space", category="Actions"
|
59
|
+
),
|
50
60
|
HotkeyAction("save_output", "Save Output", "Return", category="Actions"),
|
51
|
-
HotkeyAction(
|
61
|
+
HotkeyAction(
|
62
|
+
"save_output_alt", "Save Output (Alt)", "Enter", category="Actions"
|
63
|
+
),
|
52
64
|
HotkeyAction("undo", "Undo Last Action", "Ctrl+Z", category="Actions"),
|
53
|
-
HotkeyAction(
|
54
|
-
|
65
|
+
HotkeyAction(
|
66
|
+
"redo", "Redo Last Action", "Ctrl+Y", "Ctrl+Shift+Z", category="Actions"
|
67
|
+
),
|
68
|
+
HotkeyAction(
|
69
|
+
"escape", "Cancel/Clear Selection", "Escape", category="Actions"
|
70
|
+
),
|
55
71
|
# Segments
|
56
|
-
HotkeyAction(
|
57
|
-
|
58
|
-
|
59
|
-
HotkeyAction(
|
60
|
-
|
72
|
+
HotkeyAction(
|
73
|
+
"merge_segments", "Merge Selected Segments", "M", category="Segments"
|
74
|
+
),
|
75
|
+
HotkeyAction(
|
76
|
+
"delete_segments", "Delete Selected Segments", "V", category="Segments"
|
77
|
+
),
|
78
|
+
HotkeyAction(
|
79
|
+
"delete_segments_alt",
|
80
|
+
"Delete Selected Segments (Alt)",
|
81
|
+
"Backspace",
|
82
|
+
category="Segments",
|
83
|
+
),
|
84
|
+
HotkeyAction(
|
85
|
+
"select_all", "Select All Segments", "Ctrl+A", category="Segments"
|
86
|
+
),
|
61
87
|
# View
|
62
88
|
HotkeyAction("zoom_in", "Zoom In", "Ctrl+Plus", category="View"),
|
63
89
|
HotkeyAction("zoom_out", "Zoom Out", "Ctrl+Minus", category="View"),
|
64
|
-
|
65
90
|
# Movement (WASD)
|
66
91
|
HotkeyAction("pan_up", "Pan Up", "W", category="Movement"),
|
67
92
|
HotkeyAction("pan_down", "Pan Down", "S", category="Movement"),
|
68
93
|
HotkeyAction("pan_left", "Pan Left", "A", category="Movement"),
|
69
94
|
HotkeyAction("pan_right", "Pan Right", "D", category="Movement"),
|
70
|
-
|
71
95
|
# Mouse-related (cannot be reassigned)
|
72
|
-
HotkeyAction(
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
96
|
+
HotkeyAction(
|
97
|
+
"left_click",
|
98
|
+
"Add Positive Point / Select",
|
99
|
+
"Left Click",
|
100
|
+
category="Mouse",
|
101
|
+
mouse_related=True,
|
102
|
+
),
|
103
|
+
HotkeyAction(
|
104
|
+
"right_click",
|
105
|
+
"Add Negative Point",
|
106
|
+
"Right Click",
|
107
|
+
category="Mouse",
|
108
|
+
mouse_related=True,
|
109
|
+
),
|
110
|
+
HotkeyAction(
|
111
|
+
"mouse_drag",
|
112
|
+
"Drag/Pan",
|
113
|
+
"Mouse Drag",
|
114
|
+
category="Mouse",
|
115
|
+
mouse_related=True,
|
116
|
+
),
|
78
117
|
]
|
79
|
-
|
118
|
+
|
80
119
|
for action in default_hotkeys:
|
81
120
|
self.actions[action.name] = action
|
82
|
-
|
83
|
-
def get_action(self, action_name: str) ->
|
121
|
+
|
122
|
+
def get_action(self, action_name: str) -> HotkeyAction | None:
|
84
123
|
"""Get hotkey action by name."""
|
85
124
|
return self.actions.get(action_name)
|
86
|
-
|
87
|
-
def get_actions_by_category(self) ->
|
125
|
+
|
126
|
+
def get_actions_by_category(self) -> dict[str, list[HotkeyAction]]:
|
88
127
|
"""Get actions grouped by category."""
|
89
128
|
categories = {}
|
90
129
|
for action in self.actions.values():
|
@@ -92,29 +131,29 @@ class HotkeyManager:
|
|
92
131
|
categories[action.category] = []
|
93
132
|
categories[action.category].append(action)
|
94
133
|
return categories
|
95
|
-
|
134
|
+
|
96
135
|
def set_primary_key(self, action_name: str, key: str) -> bool:
|
97
136
|
"""Set primary key for an action."""
|
98
137
|
if action_name in self.actions and not self.actions[action_name].mouse_related:
|
99
138
|
self.actions[action_name].primary_key = key
|
100
139
|
return True
|
101
140
|
return False
|
102
|
-
|
103
|
-
def set_secondary_key(self, action_name: str, key:
|
141
|
+
|
142
|
+
def set_secondary_key(self, action_name: str, key: str | None) -> bool:
|
104
143
|
"""Set secondary key for an action."""
|
105
144
|
if action_name in self.actions and not self.actions[action_name].mouse_related:
|
106
145
|
self.actions[action_name].secondary_key = key
|
107
146
|
return True
|
108
147
|
return False
|
109
|
-
|
110
|
-
def get_key_for_action(self, action_name: str) ->
|
148
|
+
|
149
|
+
def get_key_for_action(self, action_name: str) -> tuple[str | None, str | None]:
|
111
150
|
"""Get primary and secondary keys for an action."""
|
112
151
|
action = self.actions.get(action_name)
|
113
152
|
if action:
|
114
153
|
return action.primary_key, action.secondary_key
|
115
154
|
return None, None
|
116
|
-
|
117
|
-
def is_key_in_use(self, key: str, exclude_action: str = None) ->
|
155
|
+
|
156
|
+
def is_key_in_use(self, key: str, exclude_action: str = None) -> str | None:
|
118
157
|
"""Check if a key is already in use by another action."""
|
119
158
|
for name, action in self.actions.items():
|
120
159
|
if name == exclude_action:
|
@@ -122,48 +161,48 @@ class HotkeyManager:
|
|
122
161
|
if action.primary_key == key or action.secondary_key == key:
|
123
162
|
return name
|
124
163
|
return None
|
125
|
-
|
164
|
+
|
126
165
|
def reset_to_defaults(self):
|
127
166
|
"""Reset all hotkeys to default values."""
|
128
167
|
self._initialize_default_hotkeys()
|
129
|
-
|
168
|
+
|
130
169
|
def save_hotkeys(self):
|
131
170
|
"""Save hotkeys to file."""
|
132
171
|
os.makedirs(self.config_dir, exist_ok=True)
|
133
|
-
|
172
|
+
|
134
173
|
# Convert to serializable format
|
135
174
|
data = {}
|
136
175
|
for name, action in self.actions.items():
|
137
176
|
if not action.mouse_related: # Don't save mouse-related actions
|
138
177
|
data[name] = {
|
139
|
-
|
140
|
-
|
178
|
+
"primary_key": action.primary_key,
|
179
|
+
"secondary_key": action.secondary_key,
|
141
180
|
}
|
142
|
-
|
143
|
-
with open(self.hotkeys_file,
|
181
|
+
|
182
|
+
with open(self.hotkeys_file, "w") as f:
|
144
183
|
json.dump(data, f, indent=4)
|
145
|
-
|
184
|
+
|
146
185
|
def load_hotkeys(self):
|
147
186
|
"""Load hotkeys from file."""
|
148
187
|
if not os.path.exists(self.hotkeys_file):
|
149
188
|
return
|
150
|
-
|
189
|
+
|
151
190
|
try:
|
152
|
-
with open(self.hotkeys_file
|
191
|
+
with open(self.hotkeys_file) as f:
|
153
192
|
data = json.load(f)
|
154
|
-
|
193
|
+
|
155
194
|
for name, keys in data.items():
|
156
195
|
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(
|
196
|
+
self.actions[name].primary_key = keys.get("primary_key", "")
|
197
|
+
self.actions[name].secondary_key = keys.get("secondary_key")
|
159
198
|
except (json.JSONDecodeError, KeyError, FileNotFoundError):
|
160
199
|
# If loading fails, keep defaults
|
161
200
|
pass
|
162
|
-
|
201
|
+
|
163
202
|
def key_sequence_to_string(self, key_sequence: QKeySequence) -> str:
|
164
203
|
"""Convert QKeySequence to string representation."""
|
165
204
|
return key_sequence.toString()
|
166
|
-
|
205
|
+
|
167
206
|
def string_to_key_sequence(self, key_string: str) -> QKeySequence:
|
168
207
|
"""Convert string to QKeySequence."""
|
169
|
-
return QKeySequence(key_string)
|
208
|
+
return QKeySequence(key_string)
|
lazylabel/config/paths.py
CHANGED
@@ -1,41 +1,40 @@
|
|
1
|
-
"""Path management for LazyLabel."""
|
2
|
-
|
3
|
-
import
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
self.
|
12
|
-
self.
|
13
|
-
self.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
self.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
return self.cache_dir / filename
|
1
|
+
"""Path management for LazyLabel."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
class Paths:
|
7
|
+
"""Centralized path management."""
|
8
|
+
|
9
|
+
def __init__(self):
|
10
|
+
self.app_dir = Path(__file__).parent.parent
|
11
|
+
self.models_dir = self.app_dir / "models"
|
12
|
+
self.config_dir = Path.home() / ".config" / "lazylabel"
|
13
|
+
self.cache_dir = Path.home() / ".cache" / "lazylabel"
|
14
|
+
|
15
|
+
# Ensure directories exist
|
16
|
+
self.models_dir.mkdir(exist_ok=True)
|
17
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
18
|
+
|
19
|
+
@property
|
20
|
+
def settings_file(self) -> Path:
|
21
|
+
"""Path to settings file."""
|
22
|
+
return self.config_dir / "settings.json"
|
23
|
+
|
24
|
+
@property
|
25
|
+
def demo_pictures_dir(self) -> Path:
|
26
|
+
"""Path to demo pictures directory."""
|
27
|
+
return self.app_dir / "demo_pictures"
|
28
|
+
|
29
|
+
@property
|
30
|
+
def logo_path(self) -> Path:
|
31
|
+
"""Path to application logo."""
|
32
|
+
return self.demo_pictures_dir / "logo2.png"
|
33
|
+
|
34
|
+
def get_model_path(self, filename: str) -> Path:
|
35
|
+
"""Get path for a model file."""
|
36
|
+
return self.models_dir / filename
|
37
|
+
|
38
|
+
def get_old_cache_model_path(self, filename: str) -> Path:
|
39
|
+
"""Get path for model in old cache location."""
|
40
|
+
return self.cache_dir / filename
|
lazylabel/config/settings.py
CHANGED
@@ -1,66 +1,65 @@
|
|
1
|
-
"""Application settings and configuration."""
|
2
|
-
|
3
|
-
import
|
4
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
DEFAULT_SETTINGS = Settings()
|
1
|
+
"""Application settings and configuration."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
from dataclasses import asdict, dataclass
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class Settings:
|
10
|
+
"""Application settings with defaults."""
|
11
|
+
|
12
|
+
# UI Settings
|
13
|
+
window_width: int = 1600
|
14
|
+
window_height: int = 900
|
15
|
+
left_panel_width: int = 250
|
16
|
+
right_panel_width: int = 350
|
17
|
+
|
18
|
+
# Annotation Settings
|
19
|
+
point_radius: float = 0.3
|
20
|
+
line_thickness: float = 0.5
|
21
|
+
pan_multiplier: float = 1.0
|
22
|
+
polygon_join_threshold: int = 2
|
23
|
+
|
24
|
+
# Model Settings
|
25
|
+
default_model_type: str = "vit_h"
|
26
|
+
default_model_filename: str = "sam_vit_h_4b8939.pth"
|
27
|
+
|
28
|
+
# Save Settings
|
29
|
+
auto_save: bool = True
|
30
|
+
save_npz: bool = True
|
31
|
+
save_txt: bool = True
|
32
|
+
save_class_aliases: bool = False
|
33
|
+
yolo_use_alias: bool = True
|
34
|
+
|
35
|
+
# UI State
|
36
|
+
annotation_size_multiplier: float = 1.0
|
37
|
+
|
38
|
+
def save_to_file(self, filepath: str) -> None:
|
39
|
+
"""Save settings to JSON file."""
|
40
|
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
41
|
+
with open(filepath, "w") as f:
|
42
|
+
json.dump(asdict(self), f, indent=4)
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
def load_from_file(cls, filepath: str) -> "Settings":
|
46
|
+
"""Load settings from JSON file."""
|
47
|
+
if not os.path.exists(filepath):
|
48
|
+
return cls()
|
49
|
+
|
50
|
+
try:
|
51
|
+
with open(filepath) as f:
|
52
|
+
data = json.load(f)
|
53
|
+
return cls(**data)
|
54
|
+
except (json.JSONDecodeError, TypeError):
|
55
|
+
return cls()
|
56
|
+
|
57
|
+
def update(self, **kwargs) -> None:
|
58
|
+
"""Update settings with new values."""
|
59
|
+
for key, value in kwargs.items():
|
60
|
+
if hasattr(self, key):
|
61
|
+
setattr(self, key, value)
|
62
|
+
|
63
|
+
|
64
|
+
# Default settings instance
|
65
|
+
DEFAULT_SETTINGS = Settings()
|
lazylabel/core/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
"""Core business logic for LazyLabel."""
|
2
|
-
|
3
|
-
from .
|
4
|
-
from .model_manager import ModelManager
|
5
|
-
from .
|
6
|
-
|
7
|
-
__all__ = [
|
1
|
+
"""Core business logic for LazyLabel."""
|
2
|
+
|
3
|
+
from .file_manager import FileManager
|
4
|
+
from .model_manager import ModelManager
|
5
|
+
from .segment_manager import SegmentManager
|
6
|
+
|
7
|
+
__all__ = ["SegmentManager", "ModelManager", "FileManager"]
|