lazylabel-gui 1.0.9__py3-none-any.whl → 1.1.1__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 (40) hide show
  1. lazylabel/__init__.py +9 -0
  2. lazylabel/config/__init__.py +7 -0
  3. lazylabel/config/hotkeys.py +169 -0
  4. lazylabel/config/paths.py +41 -0
  5. lazylabel/config/settings.py +66 -0
  6. lazylabel/core/__init__.py +7 -0
  7. lazylabel/core/file_manager.py +106 -0
  8. lazylabel/core/model_manager.py +97 -0
  9. lazylabel/core/segment_manager.py +171 -0
  10. lazylabel/main.py +20 -1262
  11. lazylabel/models/__init__.py +5 -0
  12. lazylabel/models/sam_model.py +195 -0
  13. lazylabel/ui/__init__.py +8 -0
  14. lazylabel/ui/control_panel.py +237 -0
  15. lazylabel/{editable_vertex.py → ui/editable_vertex.py} +25 -3
  16. lazylabel/ui/hotkey_dialog.py +384 -0
  17. lazylabel/{hoverable_polygon_item.py → ui/hoverable_polygon_item.py} +17 -1
  18. lazylabel/ui/main_window.py +1546 -0
  19. lazylabel/ui/right_panel.py +315 -0
  20. lazylabel/ui/widgets/__init__.py +8 -0
  21. lazylabel/ui/widgets/adjustments_widget.py +107 -0
  22. lazylabel/ui/widgets/model_selection_widget.py +94 -0
  23. lazylabel/ui/widgets/settings_widget.py +106 -0
  24. lazylabel/ui/widgets/status_bar.py +109 -0
  25. lazylabel/utils/__init__.py +6 -0
  26. lazylabel/{custom_file_system_model.py → utils/custom_file_system_model.py} +9 -3
  27. {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/METADATA +61 -11
  28. lazylabel_gui-1.1.1.dist-info/RECORD +37 -0
  29. lazylabel/controls.py +0 -265
  30. lazylabel/sam_model.py +0 -70
  31. lazylabel_gui-1.0.9.dist-info/RECORD +0 -17
  32. /lazylabel/{hoverable_pixelmap_item.py → ui/hoverable_pixelmap_item.py} +0 -0
  33. /lazylabel/{numeric_table_widget_item.py → ui/numeric_table_widget_item.py} +0 -0
  34. /lazylabel/{photo_viewer.py → ui/photo_viewer.py} +0 -0
  35. /lazylabel/{reorderable_class_table.py → ui/reorderable_class_table.py} +0 -0
  36. /lazylabel/{utils.py → utils/utils.py} +0 -0
  37. {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/WHEEL +0 -0
  38. {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/entry_points.txt +0 -0
  39. {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/licenses/LICENSE +0 -0
  40. {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,109 @@
1
+ """Status bar widget for displaying active messages."""
2
+
3
+ from PyQt6.QtWidgets import QStatusBar, QLabel
4
+ from PyQt6.QtCore import QTimer, pyqtSignal, Qt
5
+ from PyQt6.QtGui import QFont
6
+
7
+
8
+ class StatusBar(QStatusBar):
9
+ """Custom status bar for displaying messages and app status."""
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(parent)
13
+ self._message_timer = QTimer()
14
+ self._message_timer.timeout.connect(self._clear_temporary_message)
15
+ self._setup_ui()
16
+
17
+ def _setup_ui(self):
18
+ """Setup the status bar UI."""
19
+ # Set a reasonable height for the status bar
20
+ self.setFixedHeight(25)
21
+
22
+ # Main message label (centered)
23
+ self.message_label = QLabel()
24
+ self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
25
+ self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
26
+ font = QFont()
27
+ font.setPointSize(9)
28
+ self.message_label.setFont(font)
29
+
30
+ # Add the message label as the main widget
31
+ self.addWidget(self.message_label, 1) # stretch factor 1
32
+
33
+ # Permanent status label (right side)
34
+ self.permanent_label = QLabel()
35
+ self.permanent_label.setStyleSheet("color: #888; padding: 2px 5px;")
36
+ font = QFont()
37
+ font.setPointSize(9)
38
+ self.permanent_label.setFont(font)
39
+ self.addPermanentWidget(self.permanent_label)
40
+
41
+ # Default state
42
+ self.set_ready_message()
43
+
44
+ def show_message(self, message: str, duration: int = 5000):
45
+ """Show a temporary message for specified duration."""
46
+ self.message_label.setText(message)
47
+ self.message_label.setStyleSheet("color: #ffa500; padding: 2px 5px;")
48
+
49
+ # Stop any existing timer
50
+ self._message_timer.stop()
51
+
52
+ # Start new timer if duration > 0
53
+ if duration > 0:
54
+ self._message_timer.start(duration)
55
+
56
+ def show_error_message(self, message: str, duration: int = 8000):
57
+ """Show an error message with red color."""
58
+ self.message_label.setText(f"Error: {message}")
59
+ self.message_label.setStyleSheet("color: #ff6b6b; padding: 2px 5px;")
60
+
61
+ # Stop any existing timer
62
+ self._message_timer.stop()
63
+
64
+ # Start new timer if duration > 0
65
+ if duration > 0:
66
+ self._message_timer.start(duration)
67
+
68
+ def show_success_message(self, message: str, duration: int = 3000):
69
+ """Show a success message with green color."""
70
+ self.message_label.setText(message)
71
+ self.message_label.setStyleSheet("color: #51cf66; padding: 2px 5px;")
72
+
73
+ # Stop any existing timer
74
+ self._message_timer.stop()
75
+
76
+ # Start new timer if duration > 0
77
+ if duration > 0:
78
+ self._message_timer.start(duration)
79
+
80
+ def show_warning_message(self, message: str, duration: int = 5000):
81
+ """Show a warning message with yellow color."""
82
+ self.message_label.setText(f"Warning: {message}")
83
+ self.message_label.setStyleSheet("color: #ffd43b; padding: 2px 5px;")
84
+
85
+ # Stop any existing timer
86
+ self._message_timer.stop()
87
+
88
+ # Start new timer if duration > 0
89
+ if duration > 0:
90
+ self._message_timer.start(duration)
91
+
92
+ def set_permanent_message(self, message: str):
93
+ """Set a permanent message (usually for status info)."""
94
+ self.permanent_label.setText(message)
95
+
96
+ def set_ready_message(self):
97
+ """Set the default ready message."""
98
+ self.message_label.setText("") # Blank instead of "Ready"
99
+ self.message_label.setStyleSheet("color: #888; padding: 2px 5px;")
100
+ self._message_timer.stop()
101
+
102
+ def _clear_temporary_message(self):
103
+ """Clear temporary message and return to ready state."""
104
+ self.set_ready_message()
105
+ self._message_timer.stop()
106
+
107
+ def clear_message(self):
108
+ """Immediately clear any message."""
109
+ self.set_ready_message()
@@ -0,0 +1,6 @@
1
+ """Utility modules."""
2
+
3
+ from .utils import mask_to_pixmap
4
+ from .custom_file_system_model import CustomFileSystemModel
5
+
6
+ __all__ = ['mask_to_pixmap', 'CustomFileSystemModel']
@@ -39,7 +39,7 @@ class CustomFileSystemModel(QFileSystemModel):
39
39
  pass
40
40
 
41
41
  def update_cache_for_path(self, saved_file_path: str):
42
- """Incrementally updates the cache and the view for a newly saved file."""
42
+ """Incrementally updates the cache and the view for a newly saved or deleted file."""
43
43
  if not saved_file_path:
44
44
  return
45
45
 
@@ -47,9 +47,15 @@ class CustomFileSystemModel(QFileSystemModel):
47
47
  base_name = p.stem
48
48
 
49
49
  if p.suffix == ".npz":
50
- self.npz_files.add(base_name)
50
+ if p.exists():
51
+ self.npz_files.add(base_name)
52
+ else:
53
+ self.npz_files.discard(base_name)
51
54
  elif p.suffix == ".txt":
52
- self.txt_files.add(base_name)
55
+ if p.exists():
56
+ self.txt_files.add(base_name)
57
+ else:
58
+ self.txt_files.discard(base_name)
53
59
  else:
54
60
  return
55
61
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lazylabel-gui
3
- Version: 1.0.9
3
+ Version: 1.1.1
4
4
  Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
5
  Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
6
  License: MIT License
@@ -48,7 +48,8 @@ Requires-Dist: tqdm>=4.67.1
48
48
  Dynamic: license-file
49
49
 
50
50
  # <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo2.png" alt="LazyLabel Logo" style="height:60px; vertical-align:middle;" /> <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo_black.png" alt="LazyLabel Cursive" style="height:60px; vertical-align:middle;" />
51
- LazyLabel is an intuitive, AI-assisted image segmentation tool. It uses Meta's Segment Anything Model (SAM) for quick, precise mask generation, alongside advanced polygon editing for fine-tuned control. Outputs are saved in a clean, one-hot encoded `.npz` format for easy machine learning integration and in YOLO `.txt` format.
51
+
52
+ LazyLabel is an intuitive, AI-assisted image segmentation tool built with a modern, modular architecture. It uses Meta's Segment Anything Model (SAM) for quick, precise mask generation, alongside advanced polygon editing for fine-tuned control. Features comprehensive model management, customizable hotkeys, and outputs in clean, one-hot encoded `.npz` format for easy machine learning integration.
52
53
 
53
54
  Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#installation) and [Segment-Anything-UI](https://github.com/branislavhesko/segment-anything-ui/tree/main).
54
55
 
@@ -58,19 +59,33 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
58
59
 
59
60
  ## ✨ Core Features
60
61
 
61
- * **AI-Powered Segmentation**: Generate masks with simple left-click (positive) and right-click (negative) interactions.
62
- * **Vector Polygon Tool**: Full control to draw, edit, and reshape polygons. Drag vertices or move entire shapes.
63
- * **Advanced Class Management**: Assign multiple segments to a single class ID for organized labeling.
64
- * **Intuitive Editing & Refinement**: Select, merge, and re-order segments.
65
- * **Interactive UI**: Color-coded segments, sortable lists, and hover highlighting.
66
- * **Smart I/O**: Loads existing `.npz` masks; saves work as clean, one-hot encoded outputs.
62
+ ### **AI-Powered Segmentation**
63
+ * Generate masks with simple left-click (positive) and right-click (negative) interactions
64
+ * Multiple SAM model support with easy switching
65
+ * Custom model loading from any directory
66
+
67
+ ### **Advanced Editing Tools**
68
+ * **Vector Polygon Tool**: Full control to draw, edit, and reshape polygons
69
+ * **Vertex Editing**: Drag vertices or move entire shapes with precision
70
+ * **Selection & Merging**: Select, merge, and re-order segments intuitively
71
+
72
+ ### **Professional Workflow**
73
+ * **Customizable Hotkeys**: Personalize keyboard shortcuts for all functions
74
+ * **Advanced Class Management**: Assign multiple segments to single class IDs
75
+ * **Smart I/O**: Load existing `.npz` masks; save as clean, one-hot encoded outputs
76
+ * **Interactive UI**: Color-coded segments, sortable lists, and hover highlighting
77
+
78
+ ### **Modern Architecture**
79
+ * **Modular Design**: Clean, maintainable codebase with separated concerns
80
+ * **Model Management**: Dedicated model storage and switching system
81
+ * **Persistent Settings**: User preferences saved between sessions
67
82
 
68
83
  ---
69
84
 
70
85
  ## 🚀 Getting Started
71
86
 
72
87
  ### Prerequisites
73
- **Python 3.10**
88
+ **Python 3.10+**
74
89
 
75
90
  ### Installation
76
91
 
@@ -90,7 +105,7 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
90
105
  git clone https://github.com/dnzckn/LazyLabel.git
91
106
  cd LazyLabel
92
107
  ```
93
- 2. Install in editable mode, which links the installed package to your source directory:
108
+ 2. Install in editable mode:
94
109
  ```bash
95
110
  pip install -e .
96
111
  ```
@@ -99,12 +114,20 @@ Inspired by [LabelMe](https://github.com/wkentaro/labelme?tab=readme-ov-file#ins
99
114
  lazylabel-gui
100
115
  ```
101
116
 
102
- **Note**: On the first run, the application will automatically download the SAM model checkpoint (~2.5 GB) from Meta's repository to a local cache. This is a one-time download.
117
+ ### Model Management
118
+ * **Default Storage**: Models are stored in `src/lazylabel/models/` directory
119
+ * **Custom Models**: Click "Browse Models" to select custom model folders
120
+ * **Model Switching**: Use the dropdown to switch between available models
121
+ * **Auto-Detection**: Application automatically detects all `.pth` files in selected directories
122
+
123
+ **Note**: On the first run, the application will automatically download the SAM model checkpoint (~2.5 GB) from Meta's repository to the models directory. This is a one-time download.
103
124
 
104
125
  ---
105
126
 
106
127
  ## ⌨️ Controls & Keybinds
107
128
 
129
+ > **💡 Tip**: All hotkeys are fully customizable! Click the "Hotkeys" button in the control panel to personalize your shortcuts.
130
+
108
131
  ### Modes
109
132
  | Key | Action |
110
133
  |---|---|
@@ -143,5 +166,32 @@ Each channel is a binary mask for a class, combining all assigned segments into
143
166
 
144
167
  ---
145
168
 
169
+ ## 🏗️ Architecture
170
+
171
+ LazyLabel features a modern, modular architecture designed for maintainability and extensibility:
172
+
173
+ * **Modular Design**: Clean separation between UI, business logic, and configuration
174
+ * **Signal-Based Communication**: Loose coupling between components using PyQt signals
175
+ * **Persistent Configuration**: User settings and preferences saved between sessions
176
+ * **Extensible Model System**: Easy integration of new SAM models and types
177
+
178
+ For detailed technical documentation, see [ARCHITECTURE.md](src/lazylabel/ARCHITECTURE.md).
179
+
180
+ ---
181
+
182
+ ## ⌨️ Hotkey Customization
183
+
184
+ LazyLabel includes a comprehensive hotkey management system:
185
+
186
+ * **Full Customization**: Personalize keyboard shortcuts for all 27+ functions
187
+ * **Category Organization**: Hotkeys organized by function (Modes, Actions, Navigation, etc.)
188
+ * **Primary & Secondary Keys**: Set multiple shortcuts for the same action
189
+ * **Persistent Settings**: Custom hotkeys saved between sessions
190
+ * **Conflict Prevention**: System prevents duplicate key assignments
191
+
192
+ For complete hotkey documentation, see [HOTKEY_FEATURE.md](src/lazylabel/HOTKEY_FEATURE.md).
193
+
194
+ ---
195
+
146
196
  ## ☕ Support LazyLabel
147
197
  [If you found LazyLabel helpful, consider supporting the project!](https://buymeacoffee.com/dnzckn)
@@ -0,0 +1,37 @@
1
+ lazylabel/__init__.py,sha256=XTAPk-88MdkJX5IKg0f51CxNlARQ-SZIiqLneV6B3II,197
2
+ lazylabel/main.py,sha256=U0_bYNiVmmwdhRgBOryRZE91sunDQYEJTGGMNFubj1M,766
3
+ lazylabel/config/__init__.py,sha256=ahAF6cneEVmq2zyGwydGgGAOzJKkv_aumq5cWmokOms,261
4
+ lazylabel/config/hotkeys.py,sha256=LrQ4nmLWVOZvNOe4mEARFU3Wd3bZ7nvvsxGeFY6N9wA,7618
5
+ lazylabel/config/paths.py,sha256=d6lJ0rwdlnLjrhR5fIzb3Pc9Fhl4lsGIf2IZNmi4vZ8,1320
6
+ lazylabel/config/settings.py,sha256=IkUGz-FdDapF71k_LRpUOLHScglGeYjdLbbELGJ4hOs,1860
7
+ lazylabel/core/__init__.py,sha256=YIj3IAI6Er5nN7gSuo9E8eoQKbBbSq2Jy2prtU8Fp50,230
8
+ lazylabel/core/file_manager.py,sha256=cXaSPSUiqTm57hfpUxkuzgqApOZCK1RAJdfjmKRJKRA,4734
9
+ lazylabel/core/model_manager.py,sha256=pLTmQlVfambA3y4aypHu_jn_WrW0ZrMu_8j023mr_EM,3435
10
+ lazylabel/core/segment_manager.py,sha256=kLEj5S-fEmoXIiJ2Em97GJlre8OOpSotX5bIWMrBCvE,6006
11
+ lazylabel/models/__init__.py,sha256=qH0EvkWsou0boS85yM6DfRhJnrPOLc-QzWI0grAwnRI,89
12
+ lazylabel/models/sam_model.py,sha256=lMakV3_B-hNJ3BXy1sErkSzH9YvDsColevXLGaeLnP4,7762
13
+ lazylabel/ui/__init__.py,sha256=i5hgblzrydTkFSJDiFyfRZMNl4Z4J2dumuwx3bWEPvA,266
14
+ lazylabel/ui/control_panel.py,sha256=Pfv4D3V9oBkTWWzTE944JAUCa0by7jv9h3EruKnJLVM,8894
15
+ lazylabel/ui/editable_vertex.py,sha256=pMuXvlCkbguUsFisEzj3bC2UZ_xKqfGuM8kyPug5yws,1852
16
+ lazylabel/ui/hotkey_dialog.py,sha256=3QkthYil_SWYWxRGL_U8cZCsQ4y-6SExQZXJuE5aO8s,15252
17
+ lazylabel/ui/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
18
+ lazylabel/ui/hoverable_polygon_item.py,sha256=hQKax3vzhtrdB5ThwTINjOCwOYy718zw5YZPLCfLnGM,1251
19
+ lazylabel/ui/main_window.py,sha256=zjpTH-RqBx5mJ1i1Y0CC8KepgQF28mIJDYc68CUoBIk,64488
20
+ lazylabel/ui/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
21
+ lazylabel/ui/photo_viewer.py,sha256=PNgm0gU2gnIqvRkrGlQugdobGsKwAi3m3X6ZF487lCo,2055
22
+ lazylabel/ui/reorderable_class_table.py,sha256=4c-iuSkPcmk5Aey5n2zz49O85x9TQPujKG-JLxtuBCo,2406
23
+ lazylabel/ui/right_panel.py,sha256=7ksUM7eItHrdXWkuDHohXEShdYYR2FYx-zeSmbfi620,12454
24
+ lazylabel/ui/widgets/__init__.py,sha256=5wJ09bTPlhqHyBIXGDeXiNJf33YelKKLu0aPrar9AxU,314
25
+ lazylabel/ui/widgets/adjustments_widget.py,sha256=Az8GZgu4Uvhm6nEnwjiYDyShOqwieinjFnTOiUfxdzo,3940
26
+ lazylabel/ui/widgets/model_selection_widget.py,sha256=X3qVH90yCaCpLDauj-DLWJgAwAkAVQrzhG9X5mESa-o,3590
27
+ lazylabel/ui/widgets/settings_widget.py,sha256=8zhjLxUxqFqxqMYDzkXWD1Ye5V-a7HNhPjPJjYBxeZw,4361
28
+ lazylabel/ui/widgets/status_bar.py,sha256=71paNOXlxXmWbCG6luyciXRmqC7DNFYDi_Eo2HDzjOg,4026
29
+ lazylabel/utils/__init__.py,sha256=SX_aZvmFojIJ4Lskat9ly0dmFBXgN7leBdmc68aDLpg,177
30
+ lazylabel/utils/custom_file_system_model.py,sha256=zK0Z4LY2hzYVtycDWsCLEhXZ0zKZz-Qy5XGwHYrqazQ,4867
31
+ lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
32
+ lazylabel_gui-1.1.1.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
33
+ lazylabel_gui-1.1.1.dist-info/METADATA,sha256=RMI2kMGM08gRUJqkKXrMNbkKvmNDawmvjRCutT_Y0bM,8473
34
+ lazylabel_gui-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ lazylabel_gui-1.1.1.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
36
+ lazylabel_gui-1.1.1.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
37
+ lazylabel_gui-1.1.1.dist-info/RECORD,,
lazylabel/controls.py DELETED
@@ -1,265 +0,0 @@
1
- from PyQt6.QtWidgets import (
2
- QWidget,
3
- QVBoxLayout,
4
- QPushButton,
5
- QLabel,
6
- QFrame,
7
- QTableWidget,
8
- QTreeView,
9
- QAbstractItemView,
10
- QHBoxLayout,
11
- QComboBox,
12
- QHeaderView,
13
- QCheckBox,
14
- QSlider,
15
- QGroupBox,
16
- QSplitter,
17
- )
18
- from PyQt6.QtCore import Qt
19
- from .reorderable_class_table import ReorderableClassTable
20
-
21
-
22
- class ControlPanel(QWidget):
23
- def __init__(self, parent=None):
24
- super().__init__(parent)
25
- layout = QVBoxLayout(self)
26
- layout.setAlignment(Qt.AlignmentFlag.AlignTop)
27
-
28
- toggle_layout = QHBoxLayout()
29
- self.btn_toggle_visibility = QPushButton("< Hide")
30
- self.btn_toggle_visibility.setToolTip("Hide this panel")
31
- toggle_layout.addWidget(self.btn_toggle_visibility)
32
- toggle_layout.addStretch()
33
- layout.addLayout(toggle_layout)
34
-
35
- self.main_controls_widget = QWidget()
36
- main_layout = QVBoxLayout(self.main_controls_widget)
37
- main_layout.setContentsMargins(0, 0, 0, 0)
38
-
39
- self.mode_label = QLabel("Mode: Points")
40
- font = self.mode_label.font()
41
- font.setPointSize(14)
42
- font.setBold(True)
43
- self.mode_label.setFont(font)
44
- main_layout.addWidget(self.mode_label)
45
-
46
- self.btn_sam_mode = QPushButton("Point Mode (1)")
47
- self.btn_sam_mode.setToolTip("Switch to Point Mode for AI segmentation (1)")
48
- self.btn_polygon_mode = QPushButton("Polygon Mode (2)")
49
- self.btn_polygon_mode.setToolTip("Switch to Polygon Drawing Mode (2)")
50
- self.btn_selection_mode = QPushButton("Selection Mode (E)")
51
- self.btn_selection_mode.setToolTip("Toggle segment selection (E)")
52
- main_layout.addWidget(self.btn_sam_mode)
53
- main_layout.addWidget(self.btn_polygon_mode)
54
- main_layout.addWidget(self.btn_selection_mode)
55
-
56
- main_layout.addSpacing(20)
57
- line1 = QFrame()
58
- line1.setFrameShape(QFrame.Shape.HLine)
59
- main_layout.addWidget(line1)
60
- main_layout.addSpacing(10)
61
-
62
- self.btn_fit_view = QPushButton("Fit View (.)")
63
- self.btn_fit_view.setToolTip("Reset image zoom and pan to fit the view (.)")
64
- self.btn_clear_points = QPushButton("Clear Clicks (C)")
65
- self.btn_clear_points.setToolTip("Clear current temporary points/vertices (C)")
66
- main_layout.addWidget(self.btn_fit_view)
67
- main_layout.addWidget(self.btn_clear_points)
68
-
69
- main_layout.addSpacing(10)
70
-
71
- settings_group = QGroupBox("Settings")
72
- settings_layout = QVBoxLayout()
73
-
74
- self.chk_auto_save = QCheckBox("Auto-Save on Navigate")
75
- self.chk_auto_save.setToolTip(
76
- "Automatically save work when using arrow keys to change images."
77
- )
78
- self.chk_auto_save.setChecked(True)
79
- settings_layout.addWidget(self.chk_auto_save)
80
-
81
- self.chk_save_npz = QCheckBox("Save .npz")
82
- self.chk_save_npz.setChecked(True)
83
- self.chk_save_npz.setToolTip(
84
- "Save the final mask as a compressed NumPy NPZ file."
85
- )
86
- settings_layout.addWidget(self.chk_save_npz)
87
-
88
- self.chk_save_txt = QCheckBox("Save .txt")
89
- self.chk_save_txt.setChecked(True)
90
- self.chk_save_txt.setToolTip(
91
- "Save bounding box annotations in YOLO TXT format."
92
- )
93
- settings_layout.addWidget(self.chk_save_txt)
94
-
95
- self.chk_yolo_use_alias = QCheckBox("Save YOLO with Class Aliases")
96
- self.chk_yolo_use_alias.setToolTip(
97
- "If checked, saves YOLO .txt files using class alias names instead of numeric IDs.\nThis is useful when a separate .yaml or .names file defines the classes."
98
- )
99
- self.chk_yolo_use_alias.setChecked(True)
100
- settings_layout.addWidget(self.chk_yolo_use_alias)
101
-
102
- self.chk_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
103
- self.chk_save_class_aliases.setToolTip(
104
- "Save class aliases to a companion JSON file."
105
- )
106
- self.chk_save_class_aliases.setChecked(False)
107
- settings_layout.addWidget(self.chk_save_class_aliases)
108
-
109
- settings_group.setLayout(settings_layout)
110
- main_layout.addWidget(settings_group)
111
-
112
- sliders_group = QGroupBox("Adjustments")
113
- sliders_layout = QVBoxLayout()
114
-
115
- self.size_label = QLabel("Annotation Size: 1.0x")
116
- self.size_slider = QSlider(Qt.Orientation.Horizontal)
117
- self.size_slider.setRange(1, 50)
118
- self.size_slider.setValue(10)
119
- self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
120
- sliders_layout.addWidget(self.size_label)
121
- sliders_layout.addWidget(self.size_slider)
122
-
123
- sliders_layout.addSpacing(10)
124
-
125
- self.pan_label = QLabel("Pan Speed: 1.0x")
126
- self.pan_slider = QSlider(Qt.Orientation.Horizontal)
127
- self.pan_slider.setRange(1, 100)
128
- self.pan_slider.setValue(10)
129
- self.pan_slider.setToolTip(
130
- "Adjusts the speed of WASD panning. Hold Shift for 5x boost."
131
- )
132
- sliders_layout.addWidget(self.pan_label)
133
- sliders_layout.addWidget(self.pan_slider)
134
-
135
- sliders_layout.addSpacing(10)
136
-
137
- self.join_label = QLabel("Polygon Join Distance: 2px")
138
- self.join_slider = QSlider(Qt.Orientation.Horizontal)
139
- self.join_slider.setRange(1, 10)
140
- self.join_slider.setValue(2)
141
- self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
142
- sliders_layout.addWidget(self.join_label)
143
- sliders_layout.addWidget(self.join_slider)
144
-
145
- sliders_group.setLayout(sliders_layout)
146
- main_layout.addWidget(sliders_group)
147
-
148
- main_layout.addStretch()
149
-
150
- self.notification_label = QLabel("")
151
- font = self.notification_label.font()
152
- font.setItalic(True)
153
- self.notification_label.setFont(font)
154
- self.notification_label.setStyleSheet("color: #ffa500;")
155
- self.notification_label.setWordWrap(True)
156
- main_layout.addWidget(self.notification_label)
157
-
158
- self.device_label = QLabel("Device: Unknown")
159
- main_layout.addWidget(self.device_label)
160
-
161
- layout.addWidget(self.main_controls_widget)
162
- self.setFixedWidth(250)
163
-
164
-
165
- class RightPanel(QWidget):
166
- def __init__(self, parent=None):
167
- super().__init__(parent)
168
- self.v_layout = QVBoxLayout(self)
169
-
170
- toggle_layout = QHBoxLayout()
171
- toggle_layout.addStretch()
172
- self.btn_toggle_visibility = QPushButton("Hide >")
173
- self.btn_toggle_visibility.setToolTip("Hide this panel")
174
- toggle_layout.addWidget(self.btn_toggle_visibility)
175
- self.v_layout.addLayout(toggle_layout)
176
-
177
- self.main_controls_widget = QWidget()
178
- main_layout = QVBoxLayout(self.main_controls_widget)
179
- main_layout.setContentsMargins(0, 0, 0, 0)
180
-
181
- v_splitter = QSplitter(Qt.Orientation.Vertical)
182
-
183
- file_explorer_widget = QWidget()
184
- file_explorer_layout = QVBoxLayout(file_explorer_widget)
185
- file_explorer_layout.setContentsMargins(0, 0, 0, 0)
186
- self.btn_open_folder = QPushButton("Open Image Folder")
187
- self.btn_open_folder.setToolTip("Open a directory of images")
188
- self.file_tree = QTreeView()
189
- file_explorer_layout.addWidget(self.btn_open_folder)
190
- file_explorer_layout.addWidget(self.file_tree)
191
- v_splitter.addWidget(file_explorer_widget)
192
-
193
- segment_widget = QWidget()
194
- segment_layout = QVBoxLayout(segment_widget)
195
- segment_layout.setContentsMargins(0, 0, 0, 0)
196
-
197
- class_filter_layout = QHBoxLayout()
198
- class_filter_layout.addWidget(QLabel("Filter Class:"))
199
- self.class_filter_combo = QComboBox()
200
- self.class_filter_combo.setToolTip("Filter segments list by class")
201
- class_filter_layout.addWidget(self.class_filter_combo)
202
- segment_layout.addLayout(class_filter_layout)
203
-
204
- self.segment_table = QTableWidget()
205
- self.segment_table.setColumnCount(3)
206
- self.segment_table.setHorizontalHeaderLabels(
207
- ["Segment ID", "Class ID", "Alias"]
208
- )
209
- self.segment_table.horizontalHeader().setSectionResizeMode(
210
- QHeaderView.ResizeMode.Stretch
211
- )
212
- self.segment_table.setSelectionBehavior(
213
- QAbstractItemView.SelectionBehavior.SelectRows
214
- )
215
- self.segment_table.setSortingEnabled(True)
216
- self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
217
- segment_layout.addWidget(self.segment_table)
218
-
219
- segment_action_layout = QHBoxLayout()
220
- self.btn_merge_selection = QPushButton("Merge to Class")
221
- self.btn_merge_selection.setToolTip(
222
- "Merge selected segments into a single class (M)"
223
- )
224
- self.btn_delete_selection = QPushButton("Delete")
225
- self.btn_delete_selection.setToolTip(
226
- "Delete selected segments (Delete/Backspace)"
227
- )
228
- segment_action_layout.addWidget(self.btn_merge_selection)
229
- segment_action_layout.addWidget(self.btn_delete_selection)
230
- segment_layout.addLayout(segment_action_layout)
231
- v_splitter.addWidget(segment_widget)
232
-
233
- class_widget = QWidget()
234
- class_layout = QVBoxLayout(class_widget)
235
- class_layout.setContentsMargins(0, 0, 0, 0)
236
- class_layout.addWidget(QLabel("Class Order:"))
237
- self.class_table = ReorderableClassTable()
238
- self.class_table.setToolTip(
239
- "Double-click to set class aliases and drag to reorder channels for saving."
240
- )
241
- self.class_table.setColumnCount(2)
242
- self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
243
- self.class_table.horizontalHeader().setSectionResizeMode(
244
- 0, QHeaderView.ResizeMode.Stretch
245
- )
246
- self.class_table.horizontalHeader().setSectionResizeMode(
247
- 1, QHeaderView.ResizeMode.ResizeToContents
248
- )
249
- self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
250
- class_layout.addWidget(self.class_table)
251
- self.btn_reassign_classes = QPushButton("Reassign Class IDs")
252
- self.btn_reassign_classes.setToolTip(
253
- "Re-index class channels based on the current order in this table"
254
- )
255
- class_layout.addWidget(self.btn_reassign_classes)
256
- v_splitter.addWidget(class_widget)
257
-
258
- main_layout.addWidget(v_splitter)
259
-
260
- self.status_label = QLabel("")
261
- self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
262
- main_layout.addWidget(self.status_label)
263
-
264
- self.v_layout.addWidget(self.main_controls_widget)
265
- self.setFixedWidth(350)
lazylabel/sam_model.py DELETED
@@ -1,70 +0,0 @@
1
- import os
2
- import cv2
3
- import numpy as np
4
- import torch
5
- import requests
6
- from tqdm import tqdm
7
- from segment_anything import sam_model_registry, SamPredictor
8
-
9
-
10
- def download_model(url, download_path):
11
- """Downloads file with a progress bar."""
12
- print(
13
- f"SAM model not found. Downloading from Meta's GitHub repository to: {download_path}"
14
- )
15
- response = requests.get(url, stream=True)
16
- response.raise_for_status()
17
- total_size_in_bytes = int(response.headers.get("content-length", 0))
18
- block_size = 1024 # 1 Kibibyte
19
-
20
- progress_bar = tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True)
21
- with open(download_path, "wb") as file:
22
- for data in response.iter_content(block_size):
23
- progress_bar.update(len(data))
24
- file.write(data)
25
- progress_bar.close()
26
-
27
- if total_size_in_bytes != 0 and progress_bar.n != total_size_in_bytes:
28
- print("ERROR, something went wrong during download")
29
-
30
-
31
- class SamModel:
32
- def __init__(self, model_type, model_filename="sam_vit_h_4b8939.pth"):
33
- self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
-
35
- # Define model URL and local cache path
36
- model_url = f"https://dl.fbaipublicfiles.com/segment_anything/{model_filename}"
37
- cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "lazylabel")
38
- os.makedirs(cache_dir, exist_ok=True)
39
- model_path = os.path.join(cache_dir, model_filename)
40
-
41
- # Download the model if it doesn't exist
42
- if not os.path.exists(model_path):
43
- download_model(model_url, model_path)
44
-
45
- print(f"Loading SAM model from {model_path}...")
46
- self.model = sam_model_registry[model_type](checkpoint=model_path).to(
47
- self.device
48
- )
49
- self.predictor = SamPredictor(self.model)
50
- self.image = None
51
- print("SAM model loaded successfully.")
52
-
53
- def set_image(self, image_path):
54
- self.image = cv2.imread(image_path)
55
- self.image = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB)
56
- self.predictor.set_image(self.image)
57
-
58
- def predict(self, positive_points, negative_points):
59
- if not positive_points:
60
- return None
61
-
62
- points = np.array(positive_points + negative_points)
63
- labels = np.array([1] * len(positive_points) + [0] * len(negative_points))
64
-
65
- masks, _, _ = self.predictor.predict(
66
- point_coords=points,
67
- point_labels=labels,
68
- multimask_output=False,
69
- )
70
- return masks[0]
@@ -1,17 +0,0 @@
1
- lazylabel/controls.py,sha256=y983bXRaeWwrCOdpxAO2zjXGv1SM8zUSrJjUL6sbss4,10832
2
- lazylabel/custom_file_system_model.py,sha256=We0Q1f3c_mg0s5Du44NspjzuIQf8Cvgy7t8jQ2c53z0,4652
3
- lazylabel/editable_vertex.py,sha256=itGcZG5MyuctGfxjINu8IJBYFFsCGuE_YtsrfHICjiw,1115
4
- lazylabel/hoverable_pixelmap_item.py,sha256=kJFOp7WXiyHpNf7l73TZjiob85jgP30b5MZvu_z5L3c,728
5
- lazylabel/hoverable_polygon_item.py,sha256=-0l8C8PfsXtJGqvZZ2qtizxHmFwO8RCwz5UfjKpDvzY,775
6
- lazylabel/main.py,sha256=Iq1_2C2j1W3oghr1pYs2SROQIPmNf5HBOvKaLcC-uD4,51663
7
- lazylabel/numeric_table_widget_item.py,sha256=dQUlIFu9syCxTGAHVIlmbgkI7aJ3f3wmDPBz1AGK9Bg,283
8
- lazylabel/photo_viewer.py,sha256=PNgm0gU2gnIqvRkrGlQugdobGsKwAi3m3X6ZF487lCo,2055
9
- lazylabel/reorderable_class_table.py,sha256=4c-iuSkPcmk5Aey5n2zz49O85x9TQPujKG-JLxtuBCo,2406
10
- lazylabel/sam_model.py,sha256=9NB51Xq1P5dIxZMBdttwwRlszlJR3U5HRs83QsPLxNE,2595
11
- lazylabel/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
12
- lazylabel_gui-1.0.9.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
13
- lazylabel_gui-1.0.9.dist-info/METADATA,sha256=9_fhalqkG4brIXovWfgoAfIV-AyRyrr5v0CSgr3yEzA,6317
14
- lazylabel_gui-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- lazylabel_gui-1.0.9.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
16
- lazylabel_gui-1.0.9.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
17
- lazylabel_gui-1.0.9.dist-info/RECORD,,
File without changes
File without changes