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.
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 +94 -0
  9. lazylabel/core/segment_manager.py +140 -0
  10. lazylabel/main.py +10 -1246
  11. lazylabel/models/__init__.py +5 -0
  12. lazylabel/models/sam_model.py +154 -0
  13. lazylabel/ui/__init__.py +8 -0
  14. lazylabel/ui/control_panel.py +220 -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 +1264 -0
  19. lazylabel/ui/right_panel.py +239 -0
  20. lazylabel/ui/widgets/__init__.py +7 -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/utils/__init__.py +6 -0
  25. lazylabel/utils/custom_file_system_model.py +132 -0
  26. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/METADATA +62 -12
  27. lazylabel_gui-1.1.0.dist-info/RECORD +36 -0
  28. lazylabel/controls.py +0 -261
  29. lazylabel/custom_file_system_model.py +0 -72
  30. lazylabel/sam_model.py +0 -70
  31. lazylabel_gui-1.0.8.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.8.dist-info → lazylabel_gui-1.1.0.dist-info}/WHEEL +0 -0
  38. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/entry_points.txt +0 -0
  39. {lazylabel_gui-1.0.8.dist-info → lazylabel_gui-1.1.0.dist-info}/licenses/LICENSE +0 -0
  40. {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.8
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
- 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,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)