lazylabel-gui 1.1.1__py3-none-any.whl → 1.1.2__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.
Files changed (38) hide show
  1. lazylabel/__init__.py +8 -8
  2. lazylabel/config/__init__.py +6 -6
  3. lazylabel/config/hotkeys.py +168 -168
  4. lazylabel/config/paths.py +40 -40
  5. lazylabel/config/settings.py +65 -65
  6. lazylabel/core/__init__.py +6 -6
  7. lazylabel/core/file_manager.py +105 -105
  8. lazylabel/core/model_manager.py +97 -97
  9. lazylabel/core/segment_manager.py +171 -171
  10. lazylabel/main.py +36 -36
  11. lazylabel/models/__init__.py +4 -4
  12. lazylabel/models/sam_model.py +195 -195
  13. lazylabel/ui/__init__.py +7 -7
  14. lazylabel/ui/control_panel.py +241 -237
  15. lazylabel/ui/editable_vertex.py +64 -51
  16. lazylabel/ui/hotkey_dialog.py +383 -383
  17. lazylabel/ui/hoverable_pixelmap_item.py +22 -22
  18. lazylabel/ui/hoverable_polygon_item.py +39 -39
  19. lazylabel/ui/main_window.py +1659 -1546
  20. lazylabel/ui/numeric_table_widget_item.py +9 -9
  21. lazylabel/ui/photo_viewer.py +54 -54
  22. lazylabel/ui/reorderable_class_table.py +61 -61
  23. lazylabel/ui/right_panel.py +315 -315
  24. lazylabel/ui/widgets/__init__.py +8 -8
  25. lazylabel/ui/widgets/adjustments_widget.py +108 -107
  26. lazylabel/ui/widgets/model_selection_widget.py +93 -93
  27. lazylabel/ui/widgets/settings_widget.py +105 -105
  28. lazylabel/ui/widgets/status_bar.py +109 -109
  29. lazylabel/utils/__init__.py +5 -5
  30. lazylabel/utils/custom_file_system_model.py +132 -132
  31. lazylabel/utils/utils.py +12 -12
  32. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/METADATA +197 -197
  33. lazylabel_gui-1.1.2.dist-info/RECORD +37 -0
  34. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/licenses/LICENSE +21 -21
  35. lazylabel_gui-1.1.1.dist-info/RECORD +0 -37
  36. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/WHEEL +0 -0
  37. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/entry_points.txt +0 -0
  38. {lazylabel_gui-1.1.1.dist-info → lazylabel_gui-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,106 +1,106 @@
1
- """File management functionality."""
2
-
3
- import os
4
- import json
5
- import numpy as np
6
- import cv2
7
- from typing import List, Dict, Any, Optional, Tuple
8
- from pathlib import Path
9
-
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(self, image_path: str, image_size: Tuple[int, int], class_order: List[int]) -> str:
20
- """Save segments as NPZ file."""
21
- final_mask_tensor = self.segment_manager.create_final_mask_tensor(image_size, class_order)
22
- npz_path = os.path.splitext(image_path)[0] + ".npz"
23
- np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
24
- return npz_path
25
-
26
- def save_yolo_txt(self, image_path: str, image_size: Tuple[int, int],
27
- class_order: List[int], class_labels: List[str]) -> Optional[str]:
28
- """Save segments as YOLO format TXT file."""
29
- final_mask_tensor = self.segment_manager.create_final_mask_tensor(image_size, class_order)
30
- output_path = os.path.splitext(image_path)[0] + ".txt"
31
- h, w = image_size
32
-
33
- yolo_annotations = []
34
- for channel in range(final_mask_tensor.shape[2]):
35
- single_channel_image = final_mask_tensor[:, :, channel]
36
- if not np.any(single_channel_image):
37
- continue
38
-
39
- contours, _ = cv2.findContours(
40
- single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
41
- )
42
-
43
- class_label = class_labels[channel]
44
- for contour in contours:
45
- x, y, width, height = cv2.boundingRect(contour)
46
- center_x = (x + width / 2) / w
47
- center_y = (y + height / 2) / h
48
- normalized_width = width / w
49
- normalized_height = height / h
50
- yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
51
- yolo_annotations.append(yolo_entry)
52
-
53
- if not yolo_annotations:
54
- return None
55
-
56
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
57
- with open(output_path, "w") as file:
58
- for annotation in yolo_annotations:
59
- file.write(annotation + "\n")
60
-
61
- return output_path
62
-
63
- def save_class_aliases(self, image_path: str) -> str:
64
- """Save class aliases as JSON file."""
65
- aliases_path = os.path.splitext(image_path)[0] + ".json"
66
- aliases_to_save = {str(k): v for k, v in self.segment_manager.class_aliases.items()}
67
- with open(aliases_path, "w") as f:
68
- json.dump(aliases_to_save, f, indent=4)
69
- return aliases_path
70
-
71
- def load_class_aliases(self, image_path: str) -> None:
72
- """Load class aliases from JSON file."""
73
- json_path = os.path.splitext(image_path)[0] + ".json"
74
- if os.path.exists(json_path):
75
- try:
76
- with open(json_path, "r") as f:
77
- loaded_aliases = json.load(f)
78
- self.segment_manager.class_aliases = {int(k): v for k, v in loaded_aliases.items()}
79
- except (json.JSONDecodeError, ValueError) as e:
80
- print(f"Error loading class aliases from {json_path}: {e}")
81
- self.segment_manager.class_aliases.clear()
82
-
83
- def load_existing_mask(self, image_path: str) -> None:
84
- """Load existing mask from NPZ file."""
85
- npz_path = os.path.splitext(image_path)[0] + ".npz"
86
- if os.path.exists(npz_path):
87
- with np.load(npz_path) as data:
88
- if "mask" in data:
89
- mask_data = data["mask"]
90
- if mask_data.ndim == 2:
91
- mask_data = np.expand_dims(mask_data, axis=-1)
92
-
93
- num_classes = mask_data.shape[2]
94
- for i in range(num_classes):
95
- class_mask = mask_data[:, :, i].astype(bool)
96
- if np.any(class_mask):
97
- self.segment_manager.add_segment({
98
- "mask": class_mask,
99
- "type": "Loaded",
100
- "vertices": None,
101
- "class_id": i,
102
- })
103
-
104
- def is_image_file(self, filepath: str) -> bool:
105
- """Check if file is a supported image format."""
1
+ """File management functionality."""
2
+
3
+ import os
4
+ import json
5
+ import numpy as np
6
+ import cv2
7
+ from typing import List, Dict, Any, Optional, Tuple
8
+ from pathlib import Path
9
+
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(self, image_path: str, image_size: Tuple[int, int], class_order: List[int]) -> str:
20
+ """Save segments as NPZ file."""
21
+ final_mask_tensor = self.segment_manager.create_final_mask_tensor(image_size, class_order)
22
+ npz_path = os.path.splitext(image_path)[0] + ".npz"
23
+ np.savez_compressed(npz_path, mask=final_mask_tensor.astype(np.uint8))
24
+ return npz_path
25
+
26
+ def save_yolo_txt(self, image_path: str, image_size: Tuple[int, int],
27
+ class_order: List[int], class_labels: List[str]) -> Optional[str]:
28
+ """Save segments as YOLO format TXT file."""
29
+ final_mask_tensor = self.segment_manager.create_final_mask_tensor(image_size, class_order)
30
+ output_path = os.path.splitext(image_path)[0] + ".txt"
31
+ h, w = image_size
32
+
33
+ yolo_annotations = []
34
+ for channel in range(final_mask_tensor.shape[2]):
35
+ single_channel_image = final_mask_tensor[:, :, channel]
36
+ if not np.any(single_channel_image):
37
+ continue
38
+
39
+ contours, _ = cv2.findContours(
40
+ single_channel_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
41
+ )
42
+
43
+ class_label = class_labels[channel]
44
+ for contour in contours:
45
+ x, y, width, height = cv2.boundingRect(contour)
46
+ center_x = (x + width / 2) / w
47
+ center_y = (y + height / 2) / h
48
+ normalized_width = width / w
49
+ normalized_height = height / h
50
+ yolo_entry = f"{class_label} {center_x} {center_y} {normalized_width} {normalized_height}"
51
+ yolo_annotations.append(yolo_entry)
52
+
53
+ if not yolo_annotations:
54
+ return None
55
+
56
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
57
+ with open(output_path, "w") as file:
58
+ for annotation in yolo_annotations:
59
+ file.write(annotation + "\n")
60
+
61
+ return output_path
62
+
63
+ def save_class_aliases(self, image_path: str) -> str:
64
+ """Save class aliases as JSON file."""
65
+ aliases_path = os.path.splitext(image_path)[0] + ".json"
66
+ aliases_to_save = {str(k): v for k, v in self.segment_manager.class_aliases.items()}
67
+ with open(aliases_path, "w") as f:
68
+ json.dump(aliases_to_save, f, indent=4)
69
+ return aliases_path
70
+
71
+ def load_class_aliases(self, image_path: str) -> None:
72
+ """Load class aliases from JSON file."""
73
+ json_path = os.path.splitext(image_path)[0] + ".json"
74
+ if os.path.exists(json_path):
75
+ try:
76
+ with open(json_path, "r") as f:
77
+ loaded_aliases = json.load(f)
78
+ self.segment_manager.class_aliases = {int(k): v for k, v in loaded_aliases.items()}
79
+ except (json.JSONDecodeError, ValueError) as e:
80
+ print(f"Error loading class aliases from {json_path}: {e}")
81
+ self.segment_manager.class_aliases.clear()
82
+
83
+ def load_existing_mask(self, image_path: str) -> None:
84
+ """Load existing mask from NPZ file."""
85
+ npz_path = os.path.splitext(image_path)[0] + ".npz"
86
+ if os.path.exists(npz_path):
87
+ with np.load(npz_path) as data:
88
+ if "mask" in data:
89
+ mask_data = data["mask"]
90
+ if mask_data.ndim == 2:
91
+ mask_data = np.expand_dims(mask_data, axis=-1)
92
+
93
+ num_classes = mask_data.shape[2]
94
+ for i in range(num_classes):
95
+ class_mask = mask_data[:, :, i].astype(bool)
96
+ if np.any(class_mask):
97
+ self.segment_manager.add_segment({
98
+ "mask": class_mask,
99
+ "type": "Loaded",
100
+ "vertices": None,
101
+ "class_id": i,
102
+ })
103
+
104
+ def is_image_file(self, filepath: str) -> bool:
105
+ """Check if file is a supported image format."""
106
106
  return filepath.lower().endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif"))
@@ -1,97 +1,97 @@
1
- """Model management functionality."""
2
-
3
- import os
4
- import glob
5
- from typing import List, Tuple, Optional, Callable
6
- from pathlib import Path
7
-
8
- from ..models.sam_model import SamModel
9
- from ..config import Paths
10
-
11
-
12
- class ModelManager:
13
- """Manages SAM model loading and selection."""
14
-
15
- def __init__(self, paths: Paths):
16
- self.paths = paths
17
- self.sam_model: Optional[SamModel] = None
18
- self.current_models_folder: Optional[str] = None
19
- self.on_model_changed: Optional[Callable[[str], None]] = None
20
-
21
- def initialize_default_model(self, model_type: str = "vit_h") -> Optional[SamModel]:
22
- """Initialize the default SAM model.
23
-
24
- Returns:
25
- SamModel instance if successful, None if failed
26
- """
27
- try:
28
- print(f"[8/20] Loading {model_type.upper()} model...")
29
- self.sam_model = SamModel(model_type=model_type)
30
- self.current_models_folder = str(self.paths.models_dir)
31
- return self.sam_model
32
- except Exception as e:
33
- print(f"[8/20] Failed to initialize default model: {e}")
34
- self.sam_model = None
35
- return None
36
-
37
- def get_available_models(self, folder_path: str) -> List[Tuple[str, str]]:
38
- """Get list of available .pth models in folder.
39
-
40
- Returns:
41
- List of (display_name, full_path) tuples
42
- """
43
- pth_files = []
44
- for root, dirs, files in os.walk(folder_path):
45
- for file in files:
46
- if file.lower().endswith(".pth"):
47
- full_path = os.path.join(root, file)
48
- rel_path = os.path.relpath(full_path, folder_path)
49
- pth_files.append((rel_path, full_path))
50
-
51
- return sorted(pth_files, key=lambda x: x[0])
52
-
53
- def detect_model_type(self, model_path: str) -> str:
54
- """Detect model type from filename."""
55
- filename = os.path.basename(model_path).lower()
56
- if "vit_l" in filename or "large" in filename:
57
- return "vit_l"
58
- elif "vit_b" in filename or "base" in filename:
59
- return "vit_b"
60
- elif "vit_h" in filename or "huge" in filename:
61
- return "vit_h"
62
- return "vit_h" # default
63
-
64
- def load_custom_model(self, model_path: str) -> bool:
65
- """Load a custom model from path.
66
-
67
- Returns:
68
- True if successful, False otherwise
69
- """
70
- if not self.sam_model:
71
- return False
72
-
73
- if not os.path.exists(model_path):
74
- return False
75
-
76
- model_type = self.detect_model_type(model_path)
77
- success = self.sam_model.load_custom_model(model_path, model_type)
78
-
79
- if success and self.on_model_changed:
80
- model_name = os.path.basename(model_path)
81
- self.on_model_changed(f"Current: {model_name}")
82
-
83
- return success
84
-
85
- def set_models_folder(self, folder_path: str) -> None:
86
- """Set the current models folder."""
87
- self.current_models_folder = folder_path
88
-
89
- def get_models_folder(self) -> Optional[str]:
90
- """Get the current models folder."""
91
- return self.current_models_folder
92
-
93
- def is_model_available(self) -> bool:
94
- """Check if a SAM model is loaded and available."""
95
- return self.sam_model is not None and getattr(
96
- self.sam_model, "is_loaded", False
97
- )
1
+ """Model management functionality."""
2
+
3
+ import os
4
+ import glob
5
+ from typing import List, Tuple, Optional, Callable
6
+ from pathlib import Path
7
+
8
+ from ..models.sam_model import SamModel
9
+ from ..config import Paths
10
+
11
+
12
+ class ModelManager:
13
+ """Manages SAM model loading and selection."""
14
+
15
+ def __init__(self, paths: Paths):
16
+ self.paths = paths
17
+ self.sam_model: Optional[SamModel] = None
18
+ self.current_models_folder: Optional[str] = None
19
+ self.on_model_changed: Optional[Callable[[str], None]] = None
20
+
21
+ def initialize_default_model(self, model_type: str = "vit_h") -> Optional[SamModel]:
22
+ """Initialize the default SAM model.
23
+
24
+ Returns:
25
+ SamModel instance if successful, None if failed
26
+ """
27
+ try:
28
+ print(f"[8/20] Loading {model_type.upper()} model...")
29
+ self.sam_model = SamModel(model_type=model_type)
30
+ self.current_models_folder = str(self.paths.models_dir)
31
+ return self.sam_model
32
+ except Exception as e:
33
+ print(f"[8/20] Failed to initialize default model: {e}")
34
+ self.sam_model = None
35
+ return None
36
+
37
+ def get_available_models(self, folder_path: str) -> List[Tuple[str, str]]:
38
+ """Get list of available .pth models in folder.
39
+
40
+ Returns:
41
+ List of (display_name, full_path) tuples
42
+ """
43
+ pth_files = []
44
+ for root, dirs, files in os.walk(folder_path):
45
+ for file in files:
46
+ if file.lower().endswith(".pth"):
47
+ full_path = os.path.join(root, file)
48
+ rel_path = os.path.relpath(full_path, folder_path)
49
+ pth_files.append((rel_path, full_path))
50
+
51
+ return sorted(pth_files, key=lambda x: x[0])
52
+
53
+ def detect_model_type(self, model_path: str) -> str:
54
+ """Detect model type from filename."""
55
+ filename = os.path.basename(model_path).lower()
56
+ if "vit_l" in filename or "large" in filename:
57
+ return "vit_l"
58
+ elif "vit_b" in filename or "base" in filename:
59
+ return "vit_b"
60
+ elif "vit_h" in filename or "huge" in filename:
61
+ return "vit_h"
62
+ return "vit_h" # default
63
+
64
+ def load_custom_model(self, model_path: str) -> bool:
65
+ """Load a custom model from path.
66
+
67
+ Returns:
68
+ True if successful, False otherwise
69
+ """
70
+ if not self.sam_model:
71
+ return False
72
+
73
+ if not os.path.exists(model_path):
74
+ return False
75
+
76
+ model_type = self.detect_model_type(model_path)
77
+ success = self.sam_model.load_custom_model(model_path, model_type)
78
+
79
+ if success and self.on_model_changed:
80
+ model_name = os.path.basename(model_path)
81
+ self.on_model_changed(f"Current: {model_name}")
82
+
83
+ return success
84
+
85
+ def set_models_folder(self, folder_path: str) -> None:
86
+ """Set the current models folder."""
87
+ self.current_models_folder = folder_path
88
+
89
+ def get_models_folder(self) -> Optional[str]:
90
+ """Get the current models folder."""
91
+ return self.current_models_folder
92
+
93
+ def is_model_available(self) -> bool:
94
+ """Check if a SAM model is loaded and available."""
95
+ return self.sam_model is not None and getattr(
96
+ self.sam_model, "is_loaded", False
97
+ )