lazylabel-gui 1.1.1__py3-none-any.whl → 1.1.3__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 +1 -1
- lazylabel/config/__init__.py +3 -3
- lazylabel/config/hotkeys.py +96 -58
- lazylabel/config/paths.py +8 -9
- lazylabel/config/settings.py +15 -16
- lazylabel/core/__init__.py +3 -3
- lazylabel/core/file_manager.py +49 -33
- lazylabel/core/model_manager.py +9 -11
- lazylabel/core/segment_manager.py +21 -22
- lazylabel/main.py +1 -0
- lazylabel/models/__init__.py +1 -1
- lazylabel/models/sam_model.py +24 -19
- lazylabel/ui/__init__.py +3 -3
- lazylabel/ui/control_panel.py +21 -19
- lazylabel/ui/editable_vertex.py +16 -3
- lazylabel/ui/hotkey_dialog.py +125 -93
- lazylabel/ui/hoverable_polygon_item.py +1 -2
- lazylabel/ui/main_window.py +290 -49
- lazylabel/ui/photo_viewer.py +4 -7
- lazylabel/ui/reorderable_class_table.py +2 -3
- lazylabel/ui/right_panel.py +15 -16
- lazylabel/ui/widgets/__init__.py +1 -1
- lazylabel/ui/widgets/adjustments_widget.py +22 -21
- lazylabel/ui/widgets/model_selection_widget.py +28 -21
- lazylabel/ui/widgets/settings_widget.py +35 -28
- lazylabel/ui/widgets/status_bar.py +2 -2
- lazylabel/utils/__init__.py +2 -2
- lazylabel/utils/custom_file_system_model.py +3 -2
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/METADATA +48 -2
- lazylabel_gui-1.1.3.dist-info/RECORD +37 -0
- lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.3.dist-info}/top_level.txt +0 -0
lazylabel/__init__.py
CHANGED
lazylabel/config/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Configuration management for LazyLabel."""
|
2
2
|
|
3
|
-
from .
|
3
|
+
from .hotkeys import HotkeyAction, HotkeyManager
|
4
4
|
from .paths import Paths
|
5
|
-
from .
|
5
|
+
from .settings import DEFAULT_SETTINGS, Settings
|
6
6
|
|
7
|
-
__all__ = [
|
7
|
+
__all__ = ["Settings", "DEFAULT_SETTINGS", "Paths", "HotkeyManager", "HotkeyAction"]
|
lazylabel/config/hotkeys.py
CHANGED
@@ -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)
|
lazylabel/config/paths.py
CHANGED
@@ -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
|
lazylabel/config/settings.py
CHANGED
@@ -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()
|
lazylabel/core/__init__.py
CHANGED
@@ -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"]
|
lazylabel/core/file_manager.py
CHANGED
@@ -1,45 +1,55 @@
|
|
1
1
|
"""File management functionality."""
|
2
2
|
|
3
|
-
import os
|
4
3
|
import json
|
5
|
-
import
|
4
|
+
import os
|
5
|
+
|
6
6
|
import cv2
|
7
|
-
|
8
|
-
from pathlib import Path
|
7
|
+
import numpy as np
|
9
8
|
|
10
9
|
from .segment_manager import SegmentManager
|
11
10
|
|
12
11
|
|
13
12
|
class FileManager:
|
14
13
|
"""Manages file operations for saving and loading."""
|
15
|
-
|
14
|
+
|
16
15
|
def __init__(self, segment_manager: SegmentManager):
|
17
16
|
self.segment_manager = segment_manager
|
18
|
-
|
19
|
-
def save_npz(
|
17
|
+
|
18
|
+
def save_npz(
|
19
|
+
self, image_path: str, image_size: tuple[int, int], class_order: list[int]
|
20
|
+
) -> str:
|
20
21
|
"""Save segments as NPZ file."""
|
21
|
-
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
22
|
+
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
23
|
+
image_size, class_order
|
24
|
+
)
|
22
25
|
npz_path = os.path.splitext(image_path)[0] + ".npz"
|
23
26
|
np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
|
24
27
|
return npz_path
|
25
|
-
|
26
|
-
def save_yolo_txt(
|
27
|
-
|
28
|
+
|
29
|
+
def save_yolo_txt(
|
30
|
+
self,
|
31
|
+
image_path: str,
|
32
|
+
image_size: tuple[int, int],
|
33
|
+
class_order: list[int],
|
34
|
+
class_labels: list[str],
|
35
|
+
) -> str | None:
|
28
36
|
"""Save segments as YOLO format TXT file."""
|
29
|
-
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
37
|
+
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
38
|
+
image_size, class_order
|
39
|
+
)
|
30
40
|
output_path = os.path.splitext(image_path)[0] + ".txt"
|
31
41
|
h, w = image_size
|
32
|
-
|
42
|
+
|
33
43
|
yolo_annotations = []
|
34
44
|
for channel in range(final_mask_tensor.shape[2]):
|
35
45
|
single_channel_image = final_mask_tensor[:, :, channel]
|
36
46
|
if not np.any(single_channel_image):
|
37
47
|
continue
|
38
|
-
|
48
|
+
|
39
49
|
contours, _ = cv2.findContours(
|
40
50
|
single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
41
51
|
)
|
42
|
-
|
52
|
+
|
43
53
|
class_label = class_labels[channel]
|
44
54
|
for contour in contours:
|
45
55
|
x, y, width, height = cv2.boundingRect(contour)
|
@@ -49,37 +59,41 @@ class FileManager:
|
|
49
59
|
normalized_height = height / h
|
50
60
|
yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
|
51
61
|
yolo_annotations.append(yolo_entry)
|
52
|
-
|
62
|
+
|
53
63
|
if not yolo_annotations:
|
54
64
|
return None
|
55
|
-
|
65
|
+
|
56
66
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
57
67
|
with open(output_path, "w") as file:
|
58
68
|
for annotation in yolo_annotations:
|
59
69
|
file.write(annotation + "\n")
|
60
|
-
|
70
|
+
|
61
71
|
return output_path
|
62
|
-
|
72
|
+
|
63
73
|
def save_class_aliases(self, image_path: str) -> str:
|
64
74
|
"""Save class aliases as JSON file."""
|
65
75
|
aliases_path = os.path.splitext(image_path)[0] + ".json"
|
66
|
-
aliases_to_save = {
|
76
|
+
aliases_to_save = {
|
77
|
+
str(k): v for k, v in self.segment_manager.class_aliases.items()
|
78
|
+
}
|
67
79
|
with open(aliases_path, "w") as f:
|
68
80
|
json.dump(aliases_to_save, f, indent=4)
|
69
81
|
return aliases_path
|
70
|
-
|
82
|
+
|
71
83
|
def load_class_aliases(self, image_path: str) -> None:
|
72
84
|
"""Load class aliases from JSON file."""
|
73
85
|
json_path = os.path.splitext(image_path)[0] + ".json"
|
74
86
|
if os.path.exists(json_path):
|
75
87
|
try:
|
76
|
-
with open(json_path
|
88
|
+
with open(json_path) as f:
|
77
89
|
loaded_aliases = json.load(f)
|
78
|
-
self.segment_manager.class_aliases = {
|
90
|
+
self.segment_manager.class_aliases = {
|
91
|
+
int(k): v for k, v in loaded_aliases.items()
|
92
|
+
}
|
79
93
|
except (json.JSONDecodeError, ValueError) as e:
|
80
94
|
print(f"Error loading class aliases from {json_path}: {e}")
|
81
95
|
self.segment_manager.class_aliases.clear()
|
82
|
-
|
96
|
+
|
83
97
|
def load_existing_mask(self, image_path: str) -> None:
|
84
98
|
"""Load existing mask from NPZ file."""
|
85
99
|
npz_path = os.path.splitext(image_path)[0] + ".npz"
|
@@ -89,18 +103,20 @@ class FileManager:
|
|
89
103
|
mask_data = data["mask"]
|
90
104
|
if mask_data.ndim == 2:
|
91
105
|
mask_data = np.expand_dims(mask_data, axis=-1)
|
92
|
-
|
106
|
+
|
93
107
|
num_classes = mask_data.shape[2]
|
94
108
|
for i in range(num_classes):
|
95
109
|
class_mask = mask_data[:, :, i].astype(bool)
|
96
110
|
if np.any(class_mask):
|
97
|
-
self.segment_manager.add_segment(
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
111
|
+
self.segment_manager.add_segment(
|
112
|
+
{
|
113
|
+
"mask": class_mask,
|
114
|
+
"type": "Loaded",
|
115
|
+
"vertices": None,
|
116
|
+
"class_id": i,
|
117
|
+
}
|
118
|
+
)
|
119
|
+
|
104
120
|
def is_image_file(self, filepath: str) -> bool:
|
105
121
|
"""Check if file is a supported image format."""
|
106
|
-
return filepath.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif"))
|
122
|
+
return filepath.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif"))
|
lazylabel/core/model_manager.py
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
"""Model management functionality."""
|
2
2
|
|
3
3
|
import os
|
4
|
-
import
|
5
|
-
from typing import List, Tuple, Optional, Callable
|
6
|
-
from pathlib import Path
|
4
|
+
from collections.abc import Callable
|
7
5
|
|
8
|
-
from ..models.sam_model import SamModel
|
9
6
|
from ..config import Paths
|
7
|
+
from ..models.sam_model import SamModel
|
10
8
|
|
11
9
|
|
12
10
|
class ModelManager:
|
@@ -14,11 +12,11 @@ class ModelManager:
|
|
14
12
|
|
15
13
|
def __init__(self, paths: Paths):
|
16
14
|
self.paths = paths
|
17
|
-
self.sam_model:
|
18
|
-
self.current_models_folder:
|
19
|
-
self.on_model_changed:
|
15
|
+
self.sam_model: SamModel | None = None
|
16
|
+
self.current_models_folder: str | None = None
|
17
|
+
self.on_model_changed: Callable[[str], None] | None = None
|
20
18
|
|
21
|
-
def initialize_default_model(self, model_type: str = "vit_h") ->
|
19
|
+
def initialize_default_model(self, model_type: str = "vit_h") -> SamModel | None:
|
22
20
|
"""Initialize the default SAM model.
|
23
21
|
|
24
22
|
Returns:
|
@@ -34,14 +32,14 @@ class ModelManager:
|
|
34
32
|
self.sam_model = None
|
35
33
|
return None
|
36
34
|
|
37
|
-
def get_available_models(self, folder_path: str) ->
|
35
|
+
def get_available_models(self, folder_path: str) -> list[tuple[str, str]]:
|
38
36
|
"""Get list of available .pth models in folder.
|
39
37
|
|
40
38
|
Returns:
|
41
39
|
List of (display_name, full_path) tuples
|
42
40
|
"""
|
43
41
|
pth_files = []
|
44
|
-
for root,
|
42
|
+
for root, _dirs, files in os.walk(folder_path):
|
45
43
|
for file in files:
|
46
44
|
if file.lower().endswith(".pth"):
|
47
45
|
full_path = os.path.join(root, file)
|
@@ -86,7 +84,7 @@ class ModelManager:
|
|
86
84
|
"""Set the current models folder."""
|
87
85
|
self.current_models_folder = folder_path
|
88
86
|
|
89
|
-
def get_models_folder(self) ->
|
87
|
+
def get_models_folder(self) -> str | None:
|
90
88
|
"""Get the current models folder."""
|
91
89
|
return self.current_models_folder
|
92
90
|
|