lazylabel-gui 1.1.4__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 ADDED
@@ -0,0 +1,6 @@
1
+ """Allow LazyLabel to be run as a module with python -m lazylabel."""
2
+
3
+ from .main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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
- # 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()
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()
@@ -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 .segment_manager import SegmentManager
10
-
11
-
12
- class FileManager:
13
- """Manages file operations for saving and loading."""
14
-
15
- def __init__(self, segment_manager: SegmentManager):
16
- self.segment_manager = segment_manager
17
-
18
- def save_npz(
19
- self, image_path: str, image_size: tuple[int, int], class_order: list[int]
20
- ) -> str:
21
- """Save segments as NPZ file."""
22
- final_mask_tensor = self.segment_manager.create_final_mask_tensor(
23
- image_size, class_order
24
- )
25
- npz_path = os.path.splitext(image_path)[0] + ".npz"
26
- np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
27
- return npz_path
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:
36
- """Save segments as YOLO format TXT file."""
37
- final_mask_tensor = self.segment_manager.create_final_mask_tensor(
38
- image_size, class_order
39
- )
40
- output_path = os.path.splitext(image_path)[0] + ".txt"
41
- h, w = image_size
42
-
43
- yolo_annotations = []
44
- for channel in range(final_mask_tensor.shape[2]):
45
- single_channel_image = final_mask_tensor[:, :, channel]
46
- if not np.any(single_channel_image):
47
- continue
48
-
49
- contours, _ = cv2.findContours(
50
- single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
51
- )
52
-
53
- class_label = class_labels[channel]
54
- for contour in contours:
55
- x, y, width, height = cv2.boundingRect(contour)
56
- center_x = (x + width / 2) / w
57
- center_y = (y + height / 2) / h
58
- normalized_width = width / w
59
- normalized_height = height / h
60
- yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
61
- yolo_annotations.append(yolo_entry)
62
-
63
- if not yolo_annotations:
64
- return None
65
-
66
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
67
- with open(output_path, "w") as file:
68
- for annotation in yolo_annotations:
69
- file.write(annotation + "\n")
70
-
71
- return output_path
72
-
73
- def save_class_aliases(self, image_path: str) -> str:
74
- """Save class aliases as JSON file."""
75
- aliases_path = os.path.splitext(image_path)[0] + ".json"
76
- aliases_to_save = {
77
- str(k): v for k, v in self.segment_manager.class_aliases.items()
78
- }
79
- with open(aliases_path, "w") as f:
80
- json.dump(aliases_to_save, f, indent=4)
81
- return aliases_path
82
-
83
- def load_class_aliases(self, image_path: str) -> None:
84
- """Load class aliases from JSON file."""
85
- json_path = os.path.splitext(image_path)[0] + ".json"
86
- if os.path.exists(json_path):
87
- try:
88
- with open(json_path) as f:
89
- loaded_aliases = json.load(f)
90
- self.segment_manager.class_aliases = {
91
- int(k): v for k, v in loaded_aliases.items()
92
- }
93
- except (json.JSONDecodeError, ValueError) as e:
94
- print(f"Error loading class aliases from {json_path}: {e}")
95
- self.segment_manager.class_aliases.clear()
96
-
97
- def load_existing_mask(self, image_path: str) -> None:
98
- """Load existing mask from NPZ file."""
99
- npz_path = os.path.splitext(image_path)[0] + ".npz"
100
- if os.path.exists(npz_path):
101
- with np.load(npz_path) as data:
102
- if "mask" in data:
103
- mask_data = data["mask"]
104
- if mask_data.ndim == 2:
105
- mask_data = np.expand_dims(mask_data, axis=-1)
106
-
107
- num_classes = mask_data.shape[2]
108
- for i in range(num_classes):
109
- class_mask = mask_data[:, :, i].astype(bool)
110
- if np.any(class_mask):
111
- self.segment_manager.add_segment(
112
- {
113
- "mask": class_mask,
114
- "type": "Loaded",
115
- "vertices": None,
116
- "class_id": i,
117
- }
118
- )
119
-
120
- def is_image_file(self, filepath: str) -> bool:
121
- """Check if file is a supported image format."""
122
- return filepath.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif"))
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"))
@@ -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
- class ModelManager:
11
- """Manages SAM model loading and selection."""
12
-
13
- def __init__(self, paths: Paths):
14
- self.paths = paths
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
18
-
19
- def initialize_default_model(self, model_type: str = "vit_h") -> SamModel | None:
20
- """Initialize the default SAM model.
21
-
22
- Returns:
23
- SamModel instance if successful, None if failed
24
- """
25
- try:
26
- print(f"[8/20] Loading {model_type.upper()} model...")
27
- self.sam_model = SamModel(model_type=model_type)
28
- self.current_models_folder = str(self.paths.models_dir)
29
- return self.sam_model
30
- except Exception as e:
31
- print(f"[8/20] Failed to initialize default model: {e}")
32
- self.sam_model = None
33
- return None
34
-
35
- def get_available_models(self, folder_path: str) -> list[tuple[str, str]]:
36
- """Get list of available .pth models in folder.
37
-
38
- Returns:
39
- List of (display_name, full_path) tuples
40
- """
41
- pth_files = []
42
- for root, _dirs, files in os.walk(folder_path):
43
- for file in files:
44
- if file.lower().endswith(".pth"):
45
- full_path = os.path.join(root, file)
46
- rel_path = os.path.relpath(full_path, folder_path)
47
- pth_files.append((rel_path, full_path))
48
-
49
- return sorted(pth_files, key=lambda x: x[0])
50
-
51
- def detect_model_type(self, model_path: str) -> str:
52
- """Detect model type from filename."""
53
- filename = os.path.basename(model_path).lower()
54
- if "vit_l" in filename or "large" in filename:
55
- return "vit_l"
56
- elif "vit_b" in filename or "base" in filename:
57
- return "vit_b"
58
- elif "vit_h" in filename or "huge" in filename:
59
- return "vit_h"
60
- return "vit_h" # default
61
-
62
- def load_custom_model(self, model_path: str) -> bool:
63
- """Load a custom model from path.
64
-
65
- Returns:
66
- True if successful, False otherwise
67
- """
68
- if not self.sam_model:
69
- return False
70
-
71
- if not os.path.exists(model_path):
72
- return False
73
-
74
- model_type = self.detect_model_type(model_path)
75
- success = self.sam_model.load_custom_model(model_path, model_type)
76
-
77
- if success and self.on_model_changed:
78
- model_name = os.path.basename(model_path)
79
- self.on_model_changed(f"Current: {model_name}")
80
-
81
- return success
82
-
83
- def set_models_folder(self, folder_path: str) -> None:
84
- """Set the current models folder."""
85
- self.current_models_folder = folder_path
86
-
87
- def get_models_folder(self) -> str | None:
88
- """Get the current models folder."""
89
- return self.current_models_folder
90
-
91
- def is_model_available(self) -> bool:
92
- """Check if a SAM model is loaded and available."""
93
- return self.sam_model is not None and getattr(
94
- self.sam_model, "is_loaded", False
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
+ )