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.
- lazylabel/__init__.py +9 -0
- lazylabel/config/__init__.py +7 -0
- lazylabel/config/hotkeys.py +169 -0
- lazylabel/config/paths.py +41 -0
- lazylabel/config/settings.py +66 -0
- lazylabel/core/__init__.py +7 -0
- lazylabel/core/file_manager.py +106 -0
- lazylabel/core/model_manager.py +97 -0
- lazylabel/core/segment_manager.py +171 -0
- lazylabel/main.py +20 -1262
- lazylabel/models/__init__.py +5 -0
- lazylabel/models/sam_model.py +195 -0
- lazylabel/ui/__init__.py +8 -0
- lazylabel/ui/control_panel.py +237 -0
- lazylabel/{editable_vertex.py → ui/editable_vertex.py} +25 -3
- lazylabel/ui/hotkey_dialog.py +384 -0
- lazylabel/{hoverable_polygon_item.py → ui/hoverable_polygon_item.py} +17 -1
- lazylabel/ui/main_window.py +1546 -0
- lazylabel/ui/right_panel.py +315 -0
- lazylabel/ui/widgets/__init__.py +8 -0
- lazylabel/ui/widgets/adjustments_widget.py +107 -0
- lazylabel/ui/widgets/model_selection_widget.py +94 -0
- lazylabel/ui/widgets/settings_widget.py +106 -0
- lazylabel/ui/widgets/status_bar.py +109 -0
- lazylabel/utils/__init__.py +6 -0
- lazylabel/{custom_file_system_model.py → utils/custom_file_system_model.py} +9 -3
- {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/METADATA +61 -11
- lazylabel_gui-1.1.1.dist-info/RECORD +37 -0
- lazylabel/controls.py +0 -265
- lazylabel/sam_model.py +0 -70
- lazylabel_gui-1.0.9.dist-info/RECORD +0 -17
- /lazylabel/{hoverable_pixelmap_item.py → ui/hoverable_pixelmap_item.py} +0 -0
- /lazylabel/{numeric_table_widget_item.py → ui/numeric_table_widget_item.py} +0 -0
- /lazylabel/{photo_viewer.py → ui/photo_viewer.py} +0 -0
- /lazylabel/{reorderable_class_table.py → ui/reorderable_class_table.py} +0 -0
- /lazylabel/{utils.py → utils/utils.py} +0 -0
- {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.0.9.dist-info → lazylabel_gui-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {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()
|
@@ -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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
62
|
-
*
|
63
|
-
*
|
64
|
-
*
|
65
|
-
|
66
|
-
|
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
|
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
|
-
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|