lazylabel-gui 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lazylabel/__init__.py +8 -8
- lazylabel/config/__init__.py +6 -6
- lazylabel/config/hotkeys.py +168 -168
- lazylabel/config/paths.py +40 -40
- lazylabel/config/settings.py +65 -65
- lazylabel/core/__init__.py +6 -6
- lazylabel/core/file_manager.py +105 -105
- lazylabel/core/model_manager.py +97 -94
- lazylabel/core/segment_manager.py +171 -140
- lazylabel/main.py +36 -22
- lazylabel/models/__init__.py +4 -4
- lazylabel/models/sam_model.py +195 -154
- lazylabel/ui/__init__.py +7 -7
- lazylabel/ui/control_panel.py +241 -220
- lazylabel/ui/editable_vertex.py +64 -51
- lazylabel/ui/hotkey_dialog.py +383 -383
- lazylabel/ui/hoverable_pixelmap_item.py +22 -22
- lazylabel/ui/hoverable_polygon_item.py +39 -39
- lazylabel/ui/main_window.py +1659 -1264
- lazylabel/ui/numeric_table_widget_item.py +9 -9
- lazylabel/ui/photo_viewer.py +54 -54
- lazylabel/ui/reorderable_class_table.py +61 -61
- lazylabel/ui/right_panel.py +315 -239
- lazylabel/ui/widgets/__init__.py +8 -7
- lazylabel/ui/widgets/adjustments_widget.py +108 -107
- lazylabel/ui/widgets/model_selection_widget.py +93 -93
- lazylabel/ui/widgets/settings_widget.py +105 -105
- lazylabel/ui/widgets/status_bar.py +109 -0
- lazylabel/utils/__init__.py +5 -5
- lazylabel/utils/custom_file_system_model.py +132 -132
- lazylabel/utils/utils.py +12 -12
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.2.dist-info}/METADATA +197 -197
- lazylabel_gui-1.1.2.dist-info/RECORD +37 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.2.dist-info}/licenses/LICENSE +21 -21
- lazylabel_gui-1.1.0.dist-info/RECORD +0 -36
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.2.dist-info}/WHEEL +0 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.2.dist-info}/entry_points.txt +0 -0
- {lazylabel_gui-1.1.0.dist-info → lazylabel_gui-1.1.2.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()
|
lazylabel/utils/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
"""Utility modules."""
|
2
|
-
|
3
|
-
from .utils import mask_to_pixmap
|
4
|
-
from .custom_file_system_model import CustomFileSystemModel
|
5
|
-
|
1
|
+
"""Utility modules."""
|
2
|
+
|
3
|
+
from .utils import mask_to_pixmap
|
4
|
+
from .custom_file_system_model import CustomFileSystemModel
|
5
|
+
|
6
6
|
__all__ = ['mask_to_pixmap', 'CustomFileSystemModel']
|
@@ -1,132 +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
|
+
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)
|
lazylabel/utils/utils.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
import numpy as np
|
2
|
-
from PyQt6.QtGui import QImage, QPixmap
|
3
|
-
|
4
|
-
|
5
|
-
def mask_to_pixmap(mask, color, alpha=150):
|
6
|
-
colored_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
|
7
|
-
colored_mask[mask, :3] = color
|
8
|
-
colored_mask[mask, 3] = alpha
|
9
|
-
image = QImage(
|
10
|
-
colored_mask.data, mask.shape[1], mask.shape[0], QImage.Format.Format_RGBA8888
|
11
|
-
)
|
12
|
-
return QPixmap.fromImage(image)
|
1
|
+
import numpy as np
|
2
|
+
from PyQt6.QtGui import QImage, QPixmap
|
3
|
+
|
4
|
+
|
5
|
+
def mask_to_pixmap(mask, color, alpha=150):
|
6
|
+
colored_mask = np.zeros((mask.shape[0], mask.shape[1], 4), dtype=np.uint8)
|
7
|
+
colored_mask[mask, :3] = color
|
8
|
+
colored_mask[mask, 3] = alpha
|
9
|
+
image = QImage(
|
10
|
+
colored_mask.data, mask.shape[1], mask.shape[0], QImage.Format.Format_RGBA8888
|
11
|
+
)
|
12
|
+
return QPixmap.fromImage(image)
|