lazylabel-gui 1.0.8__py3-none-any.whl → 1.1.0__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 +94 -0
- lazylabel/core/segment_manager.py +140 -0
- lazylabel/main.py +10 -1246
- lazylabel/models/__init__.py +5 -0
- lazylabel/models/sam_model.py +154 -0
- lazylabel/ui/__init__.py +8 -0
- lazylabel/ui/control_panel.py +220 -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 +1264 -0
- lazylabel/ui/right_panel.py +239 -0
- lazylabel/ui/widgets/__init__.py +7 -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/utils/__init__.py +6 -0
- lazylabel/utils/custom_file_system_model.py +132 -0
- {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/METADATA +62 -12
- lazylabel_gui-1.1.0.dist-info/RECORD +36 -0
- lazylabel/controls.py +0 -261
- lazylabel/custom_file_system_model.py +0 -72
- lazylabel/sam_model.py +0 -70
- lazylabel_gui-1.0.8.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.8.dist-info → lazylabel_gui-1.1.0.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,132 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from PyQt6.QtCore import Qt, QModelIndex, QDir
|
3
|
+
from PyQt6.QtGui import QFileSystemModel, QBrush, QColor
|
4
|
+
|
5
|
+
|
6
|
+
class CustomFileSystemModel(QFileSystemModel):
|
7
|
+
def __init__(self, parent=None):
|
8
|
+
super().__init__(parent)
|
9
|
+
self.setFilter(QDir.Filter.NoDotAndDotDot | QDir.Filter.Files)
|
10
|
+
self.setNameFilterDisables(False)
|
11
|
+
self.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.tiff", "*.tif"])
|
12
|
+
self.highlighted_path = None
|
13
|
+
|
14
|
+
self.npz_files = set()
|
15
|
+
self.txt_files = set()
|
16
|
+
|
17
|
+
def setRootPath(self, path: str) -> QModelIndex:
|
18
|
+
self._scan_directory(path)
|
19
|
+
return super().setRootPath(path)
|
20
|
+
|
21
|
+
def _scan_directory(self, path: str):
|
22
|
+
"""Scans the directory once and caches the basenames of .npz and .txt files."""
|
23
|
+
self.npz_files.clear()
|
24
|
+
self.txt_files.clear()
|
25
|
+
if not path:
|
26
|
+
return
|
27
|
+
|
28
|
+
directory = Path(path)
|
29
|
+
if not directory.is_dir():
|
30
|
+
return
|
31
|
+
|
32
|
+
try:
|
33
|
+
for file_path in directory.iterdir():
|
34
|
+
if file_path.suffix == ".npz":
|
35
|
+
self.npz_files.add(file_path.stem)
|
36
|
+
elif file_path.suffix == ".txt":
|
37
|
+
self.txt_files.add(file_path.stem)
|
38
|
+
except OSError:
|
39
|
+
pass
|
40
|
+
|
41
|
+
def update_cache_for_path(self, saved_file_path: str):
|
42
|
+
"""Incrementally updates the cache and the view for a newly saved or deleted file."""
|
43
|
+
if not saved_file_path:
|
44
|
+
return
|
45
|
+
|
46
|
+
p = Path(saved_file_path)
|
47
|
+
base_name = p.stem
|
48
|
+
|
49
|
+
if p.suffix == ".npz":
|
50
|
+
if p.exists():
|
51
|
+
self.npz_files.add(base_name)
|
52
|
+
else:
|
53
|
+
self.npz_files.discard(base_name)
|
54
|
+
elif p.suffix == ".txt":
|
55
|
+
if p.exists():
|
56
|
+
self.txt_files.add(base_name)
|
57
|
+
else:
|
58
|
+
self.txt_files.discard(base_name)
|
59
|
+
else:
|
60
|
+
return
|
61
|
+
|
62
|
+
# Find the model index for the corresponding image file to refresh its row
|
63
|
+
# This assumes the image file is in the same directory (the root path)
|
64
|
+
root_path = Path(self.rootPath())
|
65
|
+
for image_ext in self.nameFilters(): # e.g., '*.png', '*.jpg'
|
66
|
+
# Construct full path to the potential image file
|
67
|
+
image_file = root_path / (base_name + image_ext.replace("*", ""))
|
68
|
+
index = self.index(str(image_file))
|
69
|
+
|
70
|
+
if index.isValid() and index.row() != -1:
|
71
|
+
# Found the corresponding image file, emit signal to refresh its checkmarks
|
72
|
+
index_col1 = self.index(index.row(), 1, index.parent())
|
73
|
+
index_col2 = self.index(index.row(), 2, index.parent())
|
74
|
+
self.dataChanged.emit(
|
75
|
+
index_col1, index_col2, [Qt.ItemDataRole.CheckStateRole]
|
76
|
+
)
|
77
|
+
break
|
78
|
+
|
79
|
+
def set_highlighted_path(self, path):
|
80
|
+
self.highlighted_path = str(Path(path)) if path else None
|
81
|
+
self.layoutChanged.emit()
|
82
|
+
|
83
|
+
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
84
|
+
return 3
|
85
|
+
|
86
|
+
def headerData(
|
87
|
+
self,
|
88
|
+
section: int,
|
89
|
+
orientation: Qt.Orientation,
|
90
|
+
role: int = Qt.ItemDataRole.DisplayRole,
|
91
|
+
):
|
92
|
+
if (
|
93
|
+
orientation == Qt.Orientation.Horizontal
|
94
|
+
and role == Qt.ItemDataRole.DisplayRole
|
95
|
+
):
|
96
|
+
if section == 0:
|
97
|
+
return "File Name"
|
98
|
+
if section == 1:
|
99
|
+
return ".npz"
|
100
|
+
if section == 2:
|
101
|
+
return ".txt"
|
102
|
+
return super().headerData(section, orientation, role)
|
103
|
+
|
104
|
+
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
105
|
+
if not index.isValid():
|
106
|
+
return None
|
107
|
+
|
108
|
+
if role == Qt.ItemDataRole.BackgroundRole:
|
109
|
+
filePath = self.filePath(index)
|
110
|
+
if self.highlighted_path:
|
111
|
+
p_file = Path(filePath)
|
112
|
+
p_highlight = Path(self.highlighted_path)
|
113
|
+
if p_file.with_suffix("") == p_highlight.with_suffix(""):
|
114
|
+
return QBrush(QColor(40, 80, 40))
|
115
|
+
|
116
|
+
if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
|
117
|
+
fileName = self.fileName(index.siblingAtColumn(0))
|
118
|
+
base_name = Path(fileName).stem
|
119
|
+
|
120
|
+
if index.column() == 1:
|
121
|
+
exists = base_name in self.npz_files
|
122
|
+
elif index.column() == 2:
|
123
|
+
exists = base_name in self.txt_files
|
124
|
+
else:
|
125
|
+
return None
|
126
|
+
|
127
|
+
return Qt.CheckState.Checked if exists else Qt.CheckState.Unchecked
|
128
|
+
|
129
|
+
if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
|
130
|
+
return ""
|
131
|
+
|
132
|
+
return super().data(index, role)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lazylabel-gui
|
3
|
-
Version: 1.0
|
4
|
-
Summary: An image segmentation GUI for generating mask tensors.
|
3
|
+
Version: 1.1.0
|
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
|
7
7
|
|
@@ -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,36 @@
|
|
1
|
+
lazylabel/__init__.py,sha256=XTAPk-88MdkJX5IKg0f51CxNlARQ-SZIiqLneV6B3II,197
|
2
|
+
lazylabel/main.py,sha256=EQdIfp4GX3Qt9Hz1KFldxKiqjG0Wm0-GmuECdfGwL_8,426
|
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=Prski8UZCSj6Rkk5uCtJwhsxSSnZPos-RZwM5CgF0KY,3442
|
10
|
+
lazylabel/core/segment_manager.py,sha256=mrP6QqeHRFvX4HaX3hUsesnfWqAwUjLVfkxB8Wdy3cY,5097
|
11
|
+
lazylabel/models/__init__.py,sha256=qH0EvkWsou0boS85yM6DfRhJnrPOLc-QzWI0grAwnRI,89
|
12
|
+
lazylabel/models/sam_model.py,sha256=NPC_Zwc973jos1hV-EfUTlnX44QREgjI-QT2nDd4NUY,6408
|
13
|
+
lazylabel/ui/__init__.py,sha256=i5hgblzrydTkFSJDiFyfRZMNl4Z4J2dumuwx3bWEPvA,266
|
14
|
+
lazylabel/ui/control_panel.py,sha256=LKPfIgS12rzMbORunUpoHFGKEtEjsJTHtlU1BUAcD7g,8742
|
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=DVBmTtGd8EoiN-Qhl_Cx3o9ajNENyAf-OnYnHTs3mOI,52596
|
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=m5PBRtzwB2x8CKtLzRwVYAsp1Mq-hvIGl7KEGMCcLrg,9988
|
24
|
+
lazylabel/ui/widgets/__init__.py,sha256=O4FFoTYecZ_dUIpNjqNBVmG5b3HLhv-gYNW7cevbtsE,264
|
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/utils/__init__.py,sha256=SX_aZvmFojIJ4Lskat9ly0dmFBXgN7leBdmc68aDLpg,177
|
29
|
+
lazylabel/utils/custom_file_system_model.py,sha256=zK0Z4LY2hzYVtycDWsCLEhXZ0zKZz-Qy5XGwHYrqazQ,4867
|
30
|
+
lazylabel/utils/utils.py,sha256=sYSCoXL27OaLgOZaUkCAhgmKZ7YfhR3Cc5F8nDIa3Ig,414
|
31
|
+
lazylabel_gui-1.1.0.dist-info/licenses/LICENSE,sha256=kSDEIgrWAPd1u2UFGGpC9X71dhzrlzBFs8hbDlENnGE,1092
|
32
|
+
lazylabel_gui-1.1.0.dist-info/METADATA,sha256=HT1KUNRWJsGUjtU8OhcEcIyK8W9TiMqB_00cmpQElfE,8473
|
33
|
+
lazylabel_gui-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
34
|
+
lazylabel_gui-1.1.0.dist-info/entry_points.txt,sha256=Hd0WwEG9OPTa_ziYjiD0aRh7R6Fupt-wdQ3sspdc1mM,54
|
35
|
+
lazylabel_gui-1.1.0.dist-info/top_level.txt,sha256=YN4uIyrpDBq1wiJaBuZLDipIzyZY0jqJOmmXiPIOUkU,10
|
36
|
+
lazylabel_gui-1.1.0.dist-info/RECORD,,
|
lazylabel/controls.py
DELETED
@@ -1,261 +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_save_class_aliases = QCheckBox("Save Class Aliases (.json)")
|
96
|
-
self.chk_save_class_aliases.setToolTip(
|
97
|
-
"Save class aliases to a companion JSON file."
|
98
|
-
)
|
99
|
-
self.chk_save_class_aliases.setChecked(False)
|
100
|
-
settings_layout.addWidget(self.chk_save_class_aliases)
|
101
|
-
|
102
|
-
settings_group.setLayout(settings_layout)
|
103
|
-
main_layout.addWidget(settings_group)
|
104
|
-
|
105
|
-
sliders_group = QGroupBox("Adjustments")
|
106
|
-
sliders_layout = QVBoxLayout()
|
107
|
-
|
108
|
-
self.size_label = QLabel("Annotation Size: 1.0x")
|
109
|
-
self.size_slider = QSlider(Qt.Orientation.Horizontal)
|
110
|
-
self.size_slider.setRange(1, 50)
|
111
|
-
self.size_slider.setValue(10)
|
112
|
-
self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
|
113
|
-
sliders_layout.addWidget(self.size_label)
|
114
|
-
sliders_layout.addWidget(self.size_slider)
|
115
|
-
|
116
|
-
sliders_layout.addSpacing(10)
|
117
|
-
|
118
|
-
self.pan_label = QLabel("Pan Speed: 1.0x")
|
119
|
-
self.pan_slider = QSlider(Qt.Orientation.Horizontal)
|
120
|
-
self.pan_slider.setRange(1, 100)
|
121
|
-
self.pan_slider.setValue(10)
|
122
|
-
self.pan_slider.setToolTip(
|
123
|
-
"Adjusts the speed of WASD panning. Hold Shift for 5x boost."
|
124
|
-
)
|
125
|
-
sliders_layout.addWidget(self.pan_label)
|
126
|
-
sliders_layout.addWidget(self.pan_slider)
|
127
|
-
|
128
|
-
sliders_layout.addSpacing(10)
|
129
|
-
|
130
|
-
self.join_label = QLabel("Polygon Join Distance: 2px")
|
131
|
-
self.join_slider = QSlider(Qt.Orientation.Horizontal)
|
132
|
-
self.join_slider.setRange(1, 10)
|
133
|
-
self.join_slider.setValue(2)
|
134
|
-
self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
|
135
|
-
sliders_layout.addWidget(self.join_label)
|
136
|
-
sliders_layout.addWidget(self.join_slider)
|
137
|
-
|
138
|
-
sliders_group.setLayout(sliders_layout)
|
139
|
-
main_layout.addWidget(sliders_group)
|
140
|
-
|
141
|
-
main_layout.addStretch()
|
142
|
-
|
143
|
-
self.notification_label = QLabel("")
|
144
|
-
font = self.notification_label.font()
|
145
|
-
font.setItalic(True)
|
146
|
-
self.notification_label.setFont(font)
|
147
|
-
self.notification_label.setStyleSheet("color: #ffa500;")
|
148
|
-
self.notification_label.setWordWrap(True)
|
149
|
-
main_layout.addWidget(self.notification_label)
|
150
|
-
|
151
|
-
self.device_label = QLabel("Device: Unknown")
|
152
|
-
main_layout.addWidget(self.device_label)
|
153
|
-
|
154
|
-
layout.addWidget(self.main_controls_widget)
|
155
|
-
self.setFixedWidth(250)
|
156
|
-
|
157
|
-
|
158
|
-
class RightPanel(QWidget):
|
159
|
-
def __init__(self, parent=None):
|
160
|
-
super().__init__(parent)
|
161
|
-
self.v_layout = QVBoxLayout(self)
|
162
|
-
|
163
|
-
toggle_layout = QHBoxLayout()
|
164
|
-
toggle_layout.addStretch()
|
165
|
-
self.btn_toggle_visibility = QPushButton("Hide >")
|
166
|
-
self.btn_toggle_visibility.setToolTip("Hide this panel")
|
167
|
-
toggle_layout.addWidget(self.btn_toggle_visibility)
|
168
|
-
self.v_layout.addLayout(toggle_layout)
|
169
|
-
|
170
|
-
self.main_controls_widget = QWidget()
|
171
|
-
main_layout = QVBoxLayout(self.main_controls_widget)
|
172
|
-
main_layout.setContentsMargins(0, 0, 0, 0)
|
173
|
-
|
174
|
-
v_splitter = QSplitter(Qt.Orientation.Vertical)
|
175
|
-
|
176
|
-
# --- File Explorer Widget ---
|
177
|
-
file_explorer_widget = QWidget()
|
178
|
-
file_explorer_layout = QVBoxLayout(file_explorer_widget)
|
179
|
-
file_explorer_layout.setContentsMargins(0, 0, 0, 0)
|
180
|
-
self.btn_open_folder = QPushButton("Open Image Folder")
|
181
|
-
self.btn_open_folder.setToolTip("Open a directory of images")
|
182
|
-
self.file_tree = QTreeView()
|
183
|
-
file_explorer_layout.addWidget(self.btn_open_folder)
|
184
|
-
file_explorer_layout.addWidget(self.file_tree)
|
185
|
-
v_splitter.addWidget(file_explorer_widget)
|
186
|
-
|
187
|
-
# --- Segment List Widget ---
|
188
|
-
segment_widget = QWidget()
|
189
|
-
segment_layout = QVBoxLayout(segment_widget)
|
190
|
-
segment_layout.setContentsMargins(0, 0, 0, 0)
|
191
|
-
|
192
|
-
class_filter_layout = QHBoxLayout()
|
193
|
-
class_filter_layout.addWidget(QLabel("Filter Class:"))
|
194
|
-
self.class_filter_combo = QComboBox()
|
195
|
-
self.class_filter_combo.setToolTip("Filter segments list by class")
|
196
|
-
class_filter_layout.addWidget(self.class_filter_combo)
|
197
|
-
segment_layout.addLayout(class_filter_layout)
|
198
|
-
|
199
|
-
self.segment_table = QTableWidget()
|
200
|
-
self.segment_table.setColumnCount(3)
|
201
|
-
self.segment_table.setHorizontalHeaderLabels(
|
202
|
-
["Segment ID", "Class ID", "Alias"]
|
203
|
-
)
|
204
|
-
self.segment_table.horizontalHeader().setSectionResizeMode(
|
205
|
-
QHeaderView.ResizeMode.Stretch
|
206
|
-
)
|
207
|
-
self.segment_table.setSelectionBehavior(
|
208
|
-
QAbstractItemView.SelectionBehavior.SelectRows
|
209
|
-
)
|
210
|
-
self.segment_table.setSortingEnabled(True)
|
211
|
-
self.segment_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
212
|
-
segment_layout.addWidget(self.segment_table)
|
213
|
-
|
214
|
-
segment_action_layout = QHBoxLayout()
|
215
|
-
self.btn_merge_selection = QPushButton("Merge to Class")
|
216
|
-
self.btn_merge_selection.setToolTip(
|
217
|
-
"Merge selected segments into a single class (M)"
|
218
|
-
)
|
219
|
-
self.btn_delete_selection = QPushButton("Delete")
|
220
|
-
self.btn_delete_selection.setToolTip(
|
221
|
-
"Delete selected segments (Delete/Backspace)"
|
222
|
-
)
|
223
|
-
segment_action_layout.addWidget(self.btn_merge_selection)
|
224
|
-
segment_action_layout.addWidget(self.btn_delete_selection)
|
225
|
-
segment_layout.addLayout(segment_action_layout)
|
226
|
-
v_splitter.addWidget(segment_widget)
|
227
|
-
|
228
|
-
# --- Class Table Widget ---
|
229
|
-
class_widget = QWidget()
|
230
|
-
class_layout = QVBoxLayout(class_widget)
|
231
|
-
class_layout.setContentsMargins(0, 0, 0, 0)
|
232
|
-
class_layout.addWidget(QLabel("Class Order:"))
|
233
|
-
self.class_table = ReorderableClassTable()
|
234
|
-
self.class_table.setToolTip(
|
235
|
-
"Double-click to set class aliases and drag to reorder channels for saving."
|
236
|
-
)
|
237
|
-
self.class_table.setColumnCount(2)
|
238
|
-
self.class_table.setHorizontalHeaderLabels(["Alias", "Class ID"])
|
239
|
-
self.class_table.horizontalHeader().setSectionResizeMode(
|
240
|
-
0, QHeaderView.ResizeMode.Stretch
|
241
|
-
)
|
242
|
-
self.class_table.horizontalHeader().setSectionResizeMode(
|
243
|
-
1, QHeaderView.ResizeMode.ResizeToContents
|
244
|
-
)
|
245
|
-
self.class_table.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
|
246
|
-
class_layout.addWidget(self.class_table)
|
247
|
-
self.btn_reassign_classes = QPushButton("Reassign Class IDs")
|
248
|
-
self.btn_reassign_classes.setToolTip(
|
249
|
-
"Re-index class channels based on the current order in this table"
|
250
|
-
)
|
251
|
-
class_layout.addWidget(self.btn_reassign_classes)
|
252
|
-
v_splitter.addWidget(class_widget)
|
253
|
-
|
254
|
-
main_layout.addWidget(v_splitter)
|
255
|
-
|
256
|
-
self.status_label = QLabel("")
|
257
|
-
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
258
|
-
main_layout.addWidget(self.status_label)
|
259
|
-
|
260
|
-
self.v_layout.addWidget(self.main_controls_widget)
|
261
|
-
self.setFixedWidth(350)
|
@@ -1,72 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
from PyQt6.QtCore import Qt, QModelIndex, QDir
|
3
|
-
from PyQt6.QtGui import QFileSystemModel, QBrush, QColor
|
4
|
-
|
5
|
-
|
6
|
-
class CustomFileSystemModel(QFileSystemModel):
|
7
|
-
def __init__(self, parent=None):
|
8
|
-
super().__init__(parent)
|
9
|
-
self.setFilter(QDir.Filter.NoDotAndDotDot | QDir.Filter.Files)
|
10
|
-
self.setNameFilterDisables(False)
|
11
|
-
self.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.tiff", "*.tif"])
|
12
|
-
self.highlighted_path = None
|
13
|
-
|
14
|
-
def set_highlighted_path(self, path):
|
15
|
-
self.highlighted_path = os.path.normpath(path) if path else None
|
16
|
-
self.layoutChanged.emit()
|
17
|
-
|
18
|
-
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
19
|
-
return 3
|
20
|
-
|
21
|
-
def headerData(
|
22
|
-
self,
|
23
|
-
section: int,
|
24
|
-
orientation: Qt.Orientation,
|
25
|
-
role: int = Qt.ItemDataRole.DisplayRole,
|
26
|
-
):
|
27
|
-
if (
|
28
|
-
orientation == Qt.Orientation.Horizontal
|
29
|
-
and role == Qt.ItemDataRole.DisplayRole
|
30
|
-
):
|
31
|
-
if section == 0:
|
32
|
-
return "File Name"
|
33
|
-
if section == 1:
|
34
|
-
return ".npz"
|
35
|
-
if section == 2:
|
36
|
-
return ".txt"
|
37
|
-
return super().headerData(section, orientation, role)
|
38
|
-
|
39
|
-
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
40
|
-
if not index.isValid():
|
41
|
-
return None
|
42
|
-
|
43
|
-
if role == Qt.ItemDataRole.BackgroundRole:
|
44
|
-
filePath = os.path.normpath(self.filePath(index))
|
45
|
-
if (
|
46
|
-
self.highlighted_path
|
47
|
-
and os.path.splitext(filePath)[0]
|
48
|
-
== os.path.splitext(self.highlighted_path)[0]
|
49
|
-
):
|
50
|
-
return QBrush(QColor(40, 80, 40))
|
51
|
-
|
52
|
-
if index.column() > 0 and role == Qt.ItemDataRole.CheckStateRole:
|
53
|
-
filePath = self.filePath(index.siblingAtColumn(0))
|
54
|
-
base_path = os.path.splitext(filePath)[0]
|
55
|
-
|
56
|
-
if index.column() == 1:
|
57
|
-
check_path = base_path + ".npz"
|
58
|
-
elif index.column() == 2:
|
59
|
-
check_path = base_path + ".txt"
|
60
|
-
else:
|
61
|
-
return None
|
62
|
-
|
63
|
-
return (
|
64
|
-
Qt.CheckState.Checked
|
65
|
-
if os.path.exists(check_path)
|
66
|
-
else Qt.CheckState.Unchecked
|
67
|
-
)
|
68
|
-
|
69
|
-
if index.column() > 0 and role == Qt.ItemDataRole.DisplayRole:
|
70
|
-
return ""
|
71
|
-
|
72
|
-
return super().data(index, role)
|