lazylabel-gui 1.1.5__py3-none-any.whl → 1.1.6__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/__main__.py +6 -0
- lazylabel/config/settings.py +72 -65
- lazylabel/core/file_manager.py +123 -122
- lazylabel/core/model_manager.py +96 -95
- lazylabel/main.py +39 -37
- lazylabel/models/sam_model.py +211 -200
- lazylabel/ui/control_panel.py +283 -245
- lazylabel/ui/hotkey_dialog.py +417 -416
- lazylabel/ui/main_window.py +2020 -1882
- lazylabel/ui/photo_viewer.py +55 -1
- lazylabel/ui/widgets/adjustments_widget.py +372 -108
- lazylabel/ui/widgets/settings_widget.py +125 -113
- lazylabel/utils/logger.py +45 -0
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/METADATA +2 -1
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/RECORD +19 -17
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.1.5.dist-info → lazylabel_gui-1.1.6.dist-info}/top_level.txt +0 -0
lazylabel/__main__.py
ADDED
lazylabel/config/settings.py
CHANGED
@@ -1,65 +1,72 @@
|
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
def
|
46
|
-
"""
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
return cls()
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
+
fragment_threshold: int = 0
|
24
|
+
|
25
|
+
# Image Adjustment Settings
|
26
|
+
brightness: float = 0.0
|
27
|
+
contrast: float = 0.0
|
28
|
+
gamma: float = 1.0
|
29
|
+
|
30
|
+
# Model Settings
|
31
|
+
default_model_type: str = "vit_h"
|
32
|
+
default_model_filename: str = "sam_vit_h_4b8939.pth"
|
33
|
+
operate_on_view: bool = False
|
34
|
+
|
35
|
+
# Save Settings
|
36
|
+
auto_save: bool = True
|
37
|
+
save_npz: bool = True
|
38
|
+
save_txt: bool = True
|
39
|
+
save_class_aliases: bool = False
|
40
|
+
yolo_use_alias: bool = True
|
41
|
+
|
42
|
+
# UI State
|
43
|
+
annotation_size_multiplier: float = 1.0
|
44
|
+
|
45
|
+
def save_to_file(self, filepath: str) -> None:
|
46
|
+
"""Save settings to JSON file."""
|
47
|
+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
48
|
+
with open(filepath, "w") as f:
|
49
|
+
json.dump(asdict(self), f, indent=4)
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def load_from_file(cls, filepath: str) -> "Settings":
|
53
|
+
"""Load settings from JSON file."""
|
54
|
+
if not os.path.exists(filepath):
|
55
|
+
return cls()
|
56
|
+
|
57
|
+
try:
|
58
|
+
with open(filepath) as f:
|
59
|
+
data = json.load(f)
|
60
|
+
return cls(**data)
|
61
|
+
except (json.JSONDecodeError, TypeError):
|
62
|
+
return cls()
|
63
|
+
|
64
|
+
def update(self, **kwargs) -> None:
|
65
|
+
"""Update settings with new values."""
|
66
|
+
for key, value in kwargs.items():
|
67
|
+
if hasattr(self, key):
|
68
|
+
setattr(self, key, value)
|
69
|
+
|
70
|
+
|
71
|
+
# Default settings instance
|
72
|
+
DEFAULT_SETTINGS = Settings()
|
lazylabel/core/file_manager.py
CHANGED
@@ -1,122 +1,123 @@
|
|
1
|
-
"""File management functionality."""
|
2
|
-
|
3
|
-
import json
|
4
|
-
import os
|
5
|
-
|
6
|
-
import cv2
|
7
|
-
import numpy as np
|
8
|
-
|
9
|
-
from .
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
"
|
115
|
-
"
|
116
|
-
"
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
1
|
+
"""File management functionality."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
|
6
|
+
import cv2
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from ..utils.logger import logger
|
10
|
+
from .segment_manager import SegmentManager
|
11
|
+
|
12
|
+
|
13
|
+
class FileManager:
|
14
|
+
"""Manages file operations for saving and loading."""
|
15
|
+
|
16
|
+
def __init__(self, segment_manager: SegmentManager):
|
17
|
+
self.segment_manager = segment_manager
|
18
|
+
|
19
|
+
def save_npz(
|
20
|
+
self, image_path: str, image_size: tuple[int, int], class_order: list[int]
|
21
|
+
) -> str:
|
22
|
+
"""Save segments as NPZ file."""
|
23
|
+
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
24
|
+
image_size, class_order
|
25
|
+
)
|
26
|
+
npz_path = os.path.splitext(image_path)[0] + ".npz"
|
27
|
+
np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
|
28
|
+
return npz_path
|
29
|
+
|
30
|
+
def save_yolo_txt(
|
31
|
+
self,
|
32
|
+
image_path: str,
|
33
|
+
image_size: tuple[int, int],
|
34
|
+
class_order: list[int],
|
35
|
+
class_labels: list[str],
|
36
|
+
) -> str | None:
|
37
|
+
"""Save segments as YOLO format TXT file."""
|
38
|
+
final_mask_tensor = self.segment_manager.create_final_mask_tensor(
|
39
|
+
image_size, class_order
|
40
|
+
)
|
41
|
+
output_path = os.path.splitext(image_path)[0] + ".txt"
|
42
|
+
h, w = image_size
|
43
|
+
|
44
|
+
yolo_annotations = []
|
45
|
+
for channel in range(final_mask_tensor.shape[2]):
|
46
|
+
single_channel_image = final_mask_tensor[:, :, channel]
|
47
|
+
if not np.any(single_channel_image):
|
48
|
+
continue
|
49
|
+
|
50
|
+
contours, _ = cv2.findContours(
|
51
|
+
single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
52
|
+
)
|
53
|
+
|
54
|
+
class_label = class_labels[channel]
|
55
|
+
for contour in contours:
|
56
|
+
x, y, width, height = cv2.boundingRect(contour)
|
57
|
+
center_x = (x + width / 2) / w
|
58
|
+
center_y = (y + height / 2) / h
|
59
|
+
normalized_width = width / w
|
60
|
+
normalized_height = height / h
|
61
|
+
yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
|
62
|
+
yolo_annotations.append(yolo_entry)
|
63
|
+
|
64
|
+
if not yolo_annotations:
|
65
|
+
return None
|
66
|
+
|
67
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
68
|
+
with open(output_path, "w") as file:
|
69
|
+
for annotation in yolo_annotations:
|
70
|
+
file.write(annotation + "\n")
|
71
|
+
|
72
|
+
return output_path
|
73
|
+
|
74
|
+
def save_class_aliases(self, image_path: str) -> str:
|
75
|
+
"""Save class aliases as JSON file."""
|
76
|
+
aliases_path = os.path.splitext(image_path)[0] + ".json"
|
77
|
+
aliases_to_save = {
|
78
|
+
str(k): v for k, v in self.segment_manager.class_aliases.items()
|
79
|
+
}
|
80
|
+
with open(aliases_path, "w") as f:
|
81
|
+
json.dump(aliases_to_save, f, indent=4)
|
82
|
+
return aliases_path
|
83
|
+
|
84
|
+
def load_class_aliases(self, image_path: str) -> None:
|
85
|
+
"""Load class aliases from JSON file."""
|
86
|
+
json_path = os.path.splitext(image_path)[0] + ".json"
|
87
|
+
if os.path.exists(json_path):
|
88
|
+
try:
|
89
|
+
with open(json_path) as f:
|
90
|
+
loaded_aliases = json.load(f)
|
91
|
+
self.segment_manager.class_aliases = {
|
92
|
+
int(k): v for k, v in loaded_aliases.items()
|
93
|
+
}
|
94
|
+
except (json.JSONDecodeError, ValueError) as e:
|
95
|
+
logger.error(f"Error loading class aliases from {json_path}: {e}")
|
96
|
+
self.segment_manager.class_aliases.clear()
|
97
|
+
|
98
|
+
def load_existing_mask(self, image_path: str) -> None:
|
99
|
+
"""Load existing mask from NPZ file."""
|
100
|
+
npz_path = os.path.splitext(image_path)[0] + ".npz"
|
101
|
+
if os.path.exists(npz_path):
|
102
|
+
with np.load(npz_path) as data:
|
103
|
+
if "mask" in data:
|
104
|
+
mask_data = data["mask"]
|
105
|
+
if mask_data.ndim == 2:
|
106
|
+
mask_data = np.expand_dims(mask_data, axis=-1)
|
107
|
+
|
108
|
+
num_classes = mask_data.shape[2]
|
109
|
+
for i in range(num_classes):
|
110
|
+
class_mask = mask_data[:, :, i].astype(bool)
|
111
|
+
if np.any(class_mask):
|
112
|
+
self.segment_manager.add_segment(
|
113
|
+
{
|
114
|
+
"mask": class_mask,
|
115
|
+
"type": "Loaded",
|
116
|
+
"vertices": None,
|
117
|
+
"class_id": i,
|
118
|
+
}
|
119
|
+
)
|
120
|
+
|
121
|
+
def is_image_file(self, filepath: str) -> bool:
|
122
|
+
"""Check if file is a supported image format."""
|
123
|
+
return filepath.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif"))
|
lazylabel/core/model_manager.py
CHANGED
@@ -1,95 +1,96 @@
|
|
1
|
-
"""Model management functionality."""
|
2
|
-
|
3
|
-
import os
|
4
|
-
from collections.abc import Callable
|
5
|
-
|
6
|
-
from ..config import Paths
|
7
|
-
from ..models.sam_model import SamModel
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
self.
|
16
|
-
self.
|
17
|
-
self.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
self.
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
1
|
+
"""Model management functionality."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from collections.abc import Callable
|
5
|
+
|
6
|
+
from ..config import Paths
|
7
|
+
from ..models.sam_model import SamModel
|
8
|
+
from ..utils.logger import logger
|
9
|
+
|
10
|
+
|
11
|
+
class ModelManager:
|
12
|
+
"""Manages SAM model loading and selection."""
|
13
|
+
|
14
|
+
def __init__(self, paths: Paths):
|
15
|
+
self.paths = paths
|
16
|
+
self.sam_model: SamModel | None = None
|
17
|
+
self.current_models_folder: str | None = None
|
18
|
+
self.on_model_changed: Callable[[str], None] | None = None
|
19
|
+
|
20
|
+
def initialize_default_model(self, model_type: str = "vit_h") -> SamModel | None:
|
21
|
+
"""Initialize the default SAM model.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
SamModel instance if successful, None if failed
|
25
|
+
"""
|
26
|
+
try:
|
27
|
+
logger.info(f"Step 4/8: Loading {model_type.upper()} model...")
|
28
|
+
self.sam_model = SamModel(model_type=model_type)
|
29
|
+
self.current_models_folder = str(self.paths.models_dir)
|
30
|
+
return self.sam_model
|
31
|
+
except Exception as e:
|
32
|
+
logger.error(f"Step 4/8: Failed to initialize default model: {e}")
|
33
|
+
self.sam_model = None
|
34
|
+
return None
|
35
|
+
|
36
|
+
def get_available_models(self, folder_path: str) -> list[tuple[str, str]]:
|
37
|
+
"""Get list of available .pth models in folder.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
List of (display_name, full_path) tuples
|
41
|
+
"""
|
42
|
+
pth_files = []
|
43
|
+
for root, _dirs, files in os.walk(folder_path):
|
44
|
+
for file in files:
|
45
|
+
if file.lower().endswith(".pth"):
|
46
|
+
full_path = os.path.join(root, file)
|
47
|
+
rel_path = os.path.relpath(full_path, folder_path)
|
48
|
+
pth_files.append((rel_path, full_path))
|
49
|
+
|
50
|
+
return sorted(pth_files, key=lambda x: x[0])
|
51
|
+
|
52
|
+
def detect_model_type(self, model_path: str) -> str:
|
53
|
+
"""Detect model type from filename."""
|
54
|
+
filename = os.path.basename(model_path).lower()
|
55
|
+
if "vit_l" in filename or "large" in filename:
|
56
|
+
return "vit_l"
|
57
|
+
elif "vit_b" in filename or "base" in filename:
|
58
|
+
return "vit_b"
|
59
|
+
elif "vit_h" in filename or "huge" in filename:
|
60
|
+
return "vit_h"
|
61
|
+
return "vit_h" # default
|
62
|
+
|
63
|
+
def load_custom_model(self, model_path: str) -> bool:
|
64
|
+
"""Load a custom model from path.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
True if successful, False otherwise
|
68
|
+
"""
|
69
|
+
if not self.sam_model:
|
70
|
+
return False
|
71
|
+
|
72
|
+
if not os.path.exists(model_path):
|
73
|
+
return False
|
74
|
+
|
75
|
+
model_type = self.detect_model_type(model_path)
|
76
|
+
success = self.sam_model.load_custom_model(model_path, model_type)
|
77
|
+
|
78
|
+
if success and self.on_model_changed:
|
79
|
+
model_name = os.path.basename(model_path)
|
80
|
+
self.on_model_changed(f"Current: {model_name}")
|
81
|
+
|
82
|
+
return success
|
83
|
+
|
84
|
+
def set_models_folder(self, folder_path: str) -> None:
|
85
|
+
"""Set the current models folder."""
|
86
|
+
self.current_models_folder = folder_path
|
87
|
+
|
88
|
+
def get_models_folder(self) -> str | None:
|
89
|
+
"""Get the current models folder."""
|
90
|
+
return self.current_models_folder
|
91
|
+
|
92
|
+
def is_model_available(self) -> bool:
|
93
|
+
"""Check if a SAM model is loaded and available."""
|
94
|
+
return self.sam_model is not None and getattr(
|
95
|
+
self.sam_model, "is_loaded", False
|
96
|
+
)
|