lazylabel-gui 1.1.4__py3-none-any.whl → 1.1.6__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.
@@ -1,5 +1,7 @@
1
+ import cv2
2
+ import numpy as np
1
3
  from PyQt6.QtCore import QRectF, Qt
2
- from PyQt6.QtGui import QCursor, QPixmap
4
+ from PyQt6.QtGui import QCursor, QImage, QPixmap
3
5
  from PyQt6.QtWidgets import QGraphicsPixmapItem, QGraphicsScene, QGraphicsView
4
6
 
5
7
 
@@ -17,6 +19,10 @@ class PhotoViewer(QGraphicsView):
17
19
  self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
18
20
  self.setDragMode(QGraphicsView.DragMode.NoDrag)
19
21
 
22
+ self._original_image = None
23
+ self._adjusted_pixmap = None
24
+ self._original_image_bgr = None
25
+
20
26
  def fitInView(self, scale=True):
21
27
  rect = QRectF(self._pixmap_item.pixmap().rect())
22
28
  if not rect.isNull():
@@ -33,11 +39,59 @@ class PhotoViewer(QGraphicsView):
33
39
 
34
40
  def set_photo(self, pixmap):
35
41
  if pixmap and not pixmap.isNull():
42
+ self._original_image = pixmap.toImage()
43
+ self._adjusted_pixmap = pixmap
36
44
  self._pixmap_item.setPixmap(pixmap)
45
+
46
+ # Convert QImage to ARGB32 for consistent processing
47
+ converted_image = self._original_image.convertToFormat(
48
+ QImage.Format.Format_ARGB32
49
+ )
50
+ # Get raw bytes and reshape to numpy array (height, width, 4 for ARGB)
51
+ ptr = converted_image.constBits()
52
+ ptr.setsize(converted_image.bytesPerLine() * converted_image.height())
53
+ img_np = np.array(ptr).reshape(
54
+ converted_image.height(), converted_image.width(), 4
55
+ )
56
+ # OpenCV expects BGR, so convert from BGRA (QImage ARGB is BGRA in memory)
57
+ self._original_image_bgr = cv2.cvtColor(img_np, cv2.COLOR_BGRA2BGR)
58
+
37
59
  self.fitInView()
38
60
  else:
61
+ self._original_image = None
62
+ self._adjusted_pixmap = None
63
+ self._original_image_bgr = None
39
64
  self._pixmap_item.setPixmap(QPixmap())
40
65
 
66
+ def set_image_adjustments(self, brightness: float, contrast: float, gamma: float):
67
+ if self._original_image_bgr is None:
68
+ return
69
+
70
+ img_bgr = self._original_image_bgr.copy()
71
+
72
+ # Apply brightness and contrast
73
+ # new_image = alpha * old_image + beta
74
+ adjusted_img = cv2.convertScaleAbs(
75
+ img_bgr, alpha=1 + contrast / 100.0, beta=brightness
76
+ )
77
+
78
+ # Apply gamma correction
79
+ if gamma != 1.0:
80
+ inv_gamma = 1.0 / gamma
81
+ table = np.array(
82
+ [((i / 255.0) ** inv_gamma) * 255 for i in np.arange(0, 256)]
83
+ ).astype("uint8")
84
+ adjusted_img = cv2.LUT(adjusted_img, table)
85
+
86
+ # Convert back to QImage (BGR to RGB for QImage, then to QPixmap)
87
+ h, w, ch = adjusted_img.shape
88
+ bytes_per_line = ch * w
89
+ adjusted_qimage = QImage(
90
+ adjusted_img.data, w, h, bytes_per_line, QImage.Format.Format_BGR888
91
+ )
92
+ self._adjusted_pixmap = QPixmap.fromImage(adjusted_qimage)
93
+ self._pixmap_item.setPixmap(self._adjusted_pixmap)
94
+
41
95
  def set_cursor(self, cursor_shape):
42
96
  self.viewport().setCursor(QCursor(cursor_shape))
43
97
 
@@ -1,108 +1,372 @@
1
- """Adjustments widget for sliders and controls."""
2
-
3
- from PyQt6.QtCore import Qt, pyqtSignal
4
- from PyQt6.QtWidgets import QGroupBox, QLabel, QSlider, QVBoxLayout, QWidget
5
-
6
-
7
- class AdjustmentsWidget(QWidget):
8
- """Widget for adjustment controls."""
9
-
10
- annotation_size_changed = pyqtSignal(int)
11
- pan_speed_changed = pyqtSignal(int)
12
- join_threshold_changed = pyqtSignal(int)
13
-
14
- def __init__(self, parent=None):
15
- super().__init__(parent)
16
- self._setup_ui()
17
- self._connect_signals()
18
-
19
- def _setup_ui(self):
20
- """Setup the UI layout."""
21
- group = QGroupBox("Adjustments")
22
- layout = QVBoxLayout(group)
23
-
24
- # Annotation size
25
- self.size_label = QLabel("Annotation Size: 1.0x")
26
- self.size_slider = QSlider(Qt.Orientation.Horizontal)
27
- self.size_slider.setRange(1, 50)
28
- self.size_slider.setValue(10)
29
- self.size_slider.setToolTip("Adjusts the size of points and lines (Ctrl +/-)")
30
- layout.addWidget(self.size_label)
31
- layout.addWidget(self.size_slider)
32
-
33
- layout.addSpacing(10)
34
-
35
- # Pan speed
36
- self.pan_label = QLabel("Pan Speed: 1.0x")
37
- self.pan_slider = QSlider(Qt.Orientation.Horizontal)
38
- self.pan_slider.setRange(1, 100)
39
- self.pan_slider.setValue(10)
40
- self.pan_slider.setToolTip(
41
- "Adjusts the speed of WASD panning. Hold Shift for 5x boost."
42
- )
43
- layout.addWidget(self.pan_label)
44
- layout.addWidget(self.pan_slider)
45
-
46
- layout.addSpacing(10)
47
-
48
- # Polygon join threshold
49
- self.join_label = QLabel("Polygon Join Distance: 2px")
50
- self.join_slider = QSlider(Qt.Orientation.Horizontal)
51
- self.join_slider.setRange(1, 10)
52
- self.join_slider.setValue(2)
53
- self.join_slider.setToolTip("The pixel distance to 'snap' a polygon closed.")
54
- layout.addWidget(self.join_label)
55
- layout.addWidget(self.join_slider)
56
-
57
- # Main layout
58
- main_layout = QVBoxLayout(self)
59
- main_layout.setContentsMargins(0, 0, 0, 0)
60
- main_layout.addWidget(group)
61
-
62
- def _connect_signals(self):
63
- """Connect internal signals."""
64
- self.size_slider.valueChanged.connect(self._on_size_changed)
65
- self.pan_slider.valueChanged.connect(self._on_pan_changed)
66
- self.join_slider.valueChanged.connect(self._on_join_changed)
67
-
68
- def _on_size_changed(self, value):
69
- """Handle annotation size change."""
70
- multiplier = value / 10.0
71
- self.size_label.setText(f"Annotation Size: {multiplier:.1f}x")
72
- self.annotation_size_changed.emit(value)
73
-
74
- def _on_pan_changed(self, value):
75
- """Handle pan speed change."""
76
- multiplier = value / 10.0
77
- self.pan_label.setText(f"Pan Speed: {multiplier:.1f}x")
78
- self.pan_speed_changed.emit(value)
79
-
80
- def _on_join_changed(self, value):
81
- """Handle join threshold change."""
82
- self.join_label.setText(f"Polygon Join Distance: {value}px")
83
- self.join_threshold_changed.emit(value)
84
-
85
- def get_annotation_size(self):
86
- """Get current annotation size value."""
87
- return self.size_slider.value()
88
-
89
- def set_annotation_size(self, value):
90
- """Set annotation size value."""
91
- self.size_slider.setValue(value)
92
-
93
- def get_pan_speed(self):
94
- """Get current pan speed value."""
95
- return self.pan_slider.value()
96
-
97
- def set_pan_speed(self, value):
98
- """Set pan speed value."""
99
- self.pan_slider.setValue(value)
100
-
101
- def get_join_threshold(self):
102
- """Get current join threshold value."""
103
- return self.join_slider.value()
104
-
105
- def set_join_threshold(self, value):
106
- """Set join threshold value."""
107
- self.join_slider.setValue(value)
108
- self.join_label.setText(f"Polygon Join Distance: {value}px")
1
+ """Adjustments widget for sliders and controls."""
2
+
3
+ from PyQt6.QtCore import Qt, pyqtSignal
4
+ from PyQt6.QtWidgets import (
5
+ QGroupBox,
6
+ QHBoxLayout,
7
+ QLabel,
8
+ QLineEdit,
9
+ QPushButton,
10
+ QSlider,
11
+ QVBoxLayout,
12
+ QWidget,
13
+ )
14
+
15
+
16
+ class AdjustmentsWidget(QWidget):
17
+ """Widget for adjustment controls."""
18
+
19
+ annotation_size_changed = pyqtSignal(int)
20
+ pan_speed_changed = pyqtSignal(int)
21
+ join_threshold_changed = pyqtSignal(int)
22
+ fragment_threshold_changed = pyqtSignal(int)
23
+ brightness_changed = pyqtSignal(int)
24
+ contrast_changed = pyqtSignal(int)
25
+ gamma_changed = pyqtSignal(int)
26
+ reset_requested = pyqtSignal()
27
+ image_adjustment_changed = pyqtSignal()
28
+
29
+ def __init__(self, parent=None):
30
+ super().__init__(parent)
31
+ self._setup_ui()
32
+ self._connect_signals()
33
+
34
+ def _setup_ui(self):
35
+ """Setup the UI layout."""
36
+ group = QGroupBox("Adjustments")
37
+ layout = QVBoxLayout(group)
38
+ layout.setSpacing(3) # Reduced spacing between controls
39
+
40
+ # Helper function to create compact slider rows
41
+ def create_slider_row(
42
+ label_text, default_value, slider_range, tooltip, is_float=False
43
+ ):
44
+ row_layout = QHBoxLayout()
45
+ row_layout.setSpacing(8)
46
+
47
+ # Label with fixed width for alignment
48
+ label = QLabel(label_text)
49
+ label.setFixedWidth(80)
50
+ label.setAlignment(
51
+ Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
52
+ )
53
+
54
+ # Text edit with smaller width
55
+ text_edit = QLineEdit(str(default_value))
56
+ text_edit.setFixedWidth(35)
57
+
58
+ # Slider takes remaining space
59
+ slider = QSlider(Qt.Orientation.Horizontal)
60
+ slider.setRange(slider_range[0], slider_range[1])
61
+
62
+ # Convert default_value to appropriate type before setting slider value
63
+ if is_float:
64
+ float_value = float(default_value)
65
+ slider.setValue(int(float_value * 10))
66
+ else:
67
+ int_value = int(default_value)
68
+ slider.setValue(int_value)
69
+
70
+ slider.setToolTip(tooltip)
71
+
72
+ row_layout.addWidget(label)
73
+ row_layout.addWidget(text_edit)
74
+ row_layout.addWidget(slider, 1) # Stretch factor of 1
75
+
76
+ return row_layout, label, text_edit, slider
77
+
78
+ # Annotation size
79
+ size_row, self.size_label, self.size_edit, self.size_slider = create_slider_row(
80
+ "Size:",
81
+ "1.0",
82
+ (1, 50),
83
+ "Adjusts the size of points and lines (Ctrl +/-)",
84
+ True,
85
+ )
86
+ layout.addLayout(size_row)
87
+
88
+ # Pan speed
89
+ pan_row, self.pan_label, self.pan_edit, self.pan_slider = create_slider_row(
90
+ "Pan:",
91
+ "1.0",
92
+ (1, 100),
93
+ "Adjusts the speed of WASD panning. Hold Shift for 5x boost.",
94
+ True,
95
+ )
96
+ layout.addLayout(pan_row)
97
+
98
+ # Polygon join threshold
99
+ join_row, self.join_label, self.join_edit, self.join_slider = create_slider_row(
100
+ "Join:", "2", (1, 10), "The pixel distance to 'snap' a polygon closed."
101
+ )
102
+ layout.addLayout(join_row)
103
+
104
+ # Fragment threshold
105
+ fragment_row, self.fragment_label, self.fragment_edit, self.fragment_slider = (
106
+ create_slider_row(
107
+ "Fragment:",
108
+ "0",
109
+ (0, 100),
110
+ "Filter out small AI segments. 0=no filtering, 50=drop <50% of largest, 100=only keep largest",
111
+ )
112
+ )
113
+ layout.addLayout(fragment_row)
114
+
115
+ # Add separator for image adjustments
116
+ layout.addSpacing(8)
117
+
118
+ # Brightness
119
+ (
120
+ brightness_row,
121
+ self.brightness_label,
122
+ self.brightness_edit,
123
+ self.brightness_slider,
124
+ ) = create_slider_row("Bright:", "0", (-100, 100), "Adjust image brightness")
125
+ layout.addLayout(brightness_row)
126
+
127
+ # Contrast
128
+ contrast_row, self.contrast_label, self.contrast_edit, self.contrast_slider = (
129
+ create_slider_row("Contrast:", "0", (-100, 100), "Adjust image contrast")
130
+ )
131
+ layout.addLayout(contrast_row)
132
+
133
+ # Gamma (uses different scaling: slider_value / 100.0)
134
+ gamma_row, self.gamma_label, self.gamma_edit, self.gamma_slider = (
135
+ create_slider_row("Gamma:", "100", (1, 200), "Adjust image gamma")
136
+ )
137
+ # Set display text to show actual gamma value
138
+ self.gamma_edit.setText("1.0")
139
+ layout.addLayout(gamma_row)
140
+
141
+ # Main layout
142
+ main_layout = QVBoxLayout(self)
143
+ main_layout.setContentsMargins(0, 0, 0, 0)
144
+ main_layout.addWidget(group)
145
+
146
+ self.btn_reset = QPushButton("Reset to Defaults")
147
+ self.btn_reset.setToolTip(
148
+ "Reset all image adjustment and annotation size settings to their default values."
149
+ )
150
+ main_layout.addWidget(self.btn_reset)
151
+
152
+ def _connect_signals(self):
153
+ """Connect internal signals."""
154
+ self.size_slider.valueChanged.connect(self._on_size_slider_changed)
155
+ self.size_edit.editingFinished.connect(self._on_size_edit_finished)
156
+ self.pan_slider.valueChanged.connect(self._on_pan_slider_changed)
157
+ self.pan_edit.editingFinished.connect(self._on_pan_edit_finished)
158
+ self.join_slider.valueChanged.connect(self._on_join_slider_changed)
159
+ self.join_edit.editingFinished.connect(self._on_join_edit_finished)
160
+ self.fragment_slider.valueChanged.connect(self._on_fragment_slider_changed)
161
+ self.fragment_edit.editingFinished.connect(self._on_fragment_edit_finished)
162
+ self.brightness_slider.valueChanged.connect(self._on_brightness_slider_changed)
163
+ self.brightness_slider.sliderReleased.connect(
164
+ self._on_image_adjustment_slider_released
165
+ )
166
+ self.brightness_edit.editingFinished.connect(self._on_brightness_edit_finished)
167
+ self.contrast_slider.valueChanged.connect(self._on_contrast_slider_changed)
168
+ self.contrast_slider.sliderReleased.connect(
169
+ self._on_image_adjustment_slider_released
170
+ )
171
+ self.contrast_edit.editingFinished.connect(self._on_contrast_edit_finished)
172
+ self.gamma_slider.valueChanged.connect(self._on_gamma_slider_changed)
173
+ self.gamma_slider.sliderReleased.connect(
174
+ self._on_image_adjustment_slider_released
175
+ )
176
+ self.gamma_edit.editingFinished.connect(self._on_gamma_edit_finished)
177
+ self.btn_reset.clicked.connect(self.reset_requested)
178
+
179
+ def _on_size_slider_changed(self, value):
180
+ """Handle annotation size slider change."""
181
+ multiplier = value / 10.0
182
+ self.size_edit.setText(f"{multiplier:.1f}")
183
+ self.annotation_size_changed.emit(value)
184
+
185
+ def _on_size_edit_finished(self):
186
+ try:
187
+ value = float(self.size_edit.text())
188
+ slider_value = int(value * 10)
189
+ slider_value = max(1, min(50, slider_value))
190
+ self.size_slider.setValue(slider_value)
191
+ self.annotation_size_changed.emit(slider_value)
192
+ except ValueError:
193
+ self.size_edit.setText(f"{self.size_slider.value() / 10.0:.1f}")
194
+
195
+ def _on_pan_slider_changed(self, value):
196
+ """Handle pan speed slider change."""
197
+ multiplier = value / 10.0
198
+ self.pan_edit.setText(f"{multiplier:.1f}")
199
+ self.pan_speed_changed.emit(value)
200
+
201
+ def _on_pan_edit_finished(self):
202
+ try:
203
+ value = float(self.pan_edit.text())
204
+ slider_value = int(value * 10)
205
+ slider_value = max(1, min(100, slider_value))
206
+ self.pan_slider.setValue(slider_value)
207
+ self.pan_speed_changed.emit(slider_value)
208
+ except ValueError:
209
+ self.pan_edit.setText(f"{self.pan_slider.value() / 10.0:.1f}")
210
+
211
+ def _on_join_slider_changed(self, value):
212
+ """Handle join threshold slider change."""
213
+ self.join_edit.setText(f"{value}")
214
+ self.join_threshold_changed.emit(value)
215
+
216
+ def _on_join_edit_finished(self):
217
+ try:
218
+ value = int(self.join_edit.text())
219
+ slider_value = max(1, min(10, value))
220
+ self.join_slider.setValue(slider_value)
221
+ self.join_threshold_changed.emit(slider_value)
222
+ except ValueError:
223
+ self.join_edit.setText(f"{self.join_slider.value()}")
224
+
225
+ def _on_fragment_slider_changed(self, value):
226
+ """Handle fragment threshold slider change."""
227
+ self.fragment_edit.setText(f"{value}")
228
+ self.fragment_threshold_changed.emit(value)
229
+
230
+ def _on_fragment_edit_finished(self):
231
+ try:
232
+ value = int(self.fragment_edit.text())
233
+ slider_value = max(0, min(100, value))
234
+ self.fragment_slider.setValue(slider_value)
235
+ self.fragment_threshold_changed.emit(slider_value)
236
+ except ValueError:
237
+ self.fragment_edit.setText(f"{self.fragment_slider.value()}")
238
+
239
+ def _on_brightness_slider_changed(self, value):
240
+ """Handle brightness slider change."""
241
+ self.brightness_edit.setText(f"{value}")
242
+ self.brightness_changed.emit(value)
243
+
244
+ def _on_brightness_edit_finished(self):
245
+ try:
246
+ value = int(self.brightness_edit.text())
247
+ slider_value = max(-100, min(100, value))
248
+ self.brightness_slider.setValue(slider_value)
249
+ self.brightness_changed.emit(slider_value)
250
+ except ValueError:
251
+ self.brightness_edit.setText(f"{self.brightness_slider.value()}")
252
+
253
+ def _on_contrast_slider_changed(self, value):
254
+ """Handle contrast slider change."""
255
+ self.contrast_edit.setText(f"{value}")
256
+ self.contrast_changed.emit(value)
257
+
258
+ def _on_contrast_edit_finished(self):
259
+ try:
260
+ value = int(self.contrast_edit.text())
261
+ slider_value = max(-100, min(100, value))
262
+ self.contrast_slider.setValue(slider_value)
263
+ self.contrast_changed.emit(slider_value)
264
+ except ValueError:
265
+ self.contrast_edit.setText(f"{self.contrast_slider.value()}")
266
+
267
+ def _on_gamma_slider_changed(self, value):
268
+ """Handle gamma slider change."""
269
+ gamma_val = value / 100.0
270
+ self.gamma_edit.setText(f"{gamma_val:.2f}")
271
+ self.gamma_changed.emit(value)
272
+
273
+ def _on_gamma_edit_finished(self):
274
+ try:
275
+ value = float(self.gamma_edit.text())
276
+ slider_value = int(value * 100)
277
+ slider_value = max(1, min(200, slider_value))
278
+ self.gamma_slider.setValue(slider_value)
279
+ self.gamma_changed.emit(slider_value)
280
+ except ValueError:
281
+ self.gamma_edit.setText(f"{self.gamma_slider.value() / 100.0:.2f}")
282
+
283
+ def _on_image_adjustment_slider_released(self):
284
+ """Emit signal when any image adjustment slider is released."""
285
+ self.image_adjustment_changed.emit()
286
+
287
+ def get_annotation_size(self):
288
+ """Get current annotation size value."""
289
+ return self.size_slider.value()
290
+
291
+ def set_annotation_size(self, value):
292
+ """Set annotation size value."""
293
+
294
+ self.size_slider.setValue(value)
295
+ self.size_edit.setText(f"{value / 10.0:.1f}")
296
+
297
+ def get_pan_speed(self):
298
+ """Get current pan speed value."""
299
+
300
+ return self.pan_slider.value()
301
+
302
+ def set_pan_speed(self, value):
303
+ """Set pan speed value."""
304
+
305
+ self.pan_slider.setValue(value)
306
+ self.pan_edit.setText(f"{value / 10.0:.1f}")
307
+
308
+ def get_join_threshold(self):
309
+ """Get current join threshold value."""
310
+
311
+ return self.join_slider.value()
312
+
313
+ def set_join_threshold(self, value):
314
+ """Set join threshold value."""
315
+
316
+ self.join_slider.setValue(value)
317
+ self.join_edit.setText(f"{value}")
318
+
319
+ def get_fragment_threshold(self):
320
+ """Get current fragment threshold value."""
321
+
322
+ return self.fragment_slider.value()
323
+
324
+ def set_fragment_threshold(self, value):
325
+ """Set fragment threshold value."""
326
+
327
+ self.fragment_slider.setValue(value)
328
+ self.fragment_edit.setText(f"{value}")
329
+
330
+ def get_brightness(self):
331
+ """Get current brightness value."""
332
+
333
+ return self.brightness_slider.value()
334
+
335
+ def set_brightness(self, value):
336
+ """Set brightness value."""
337
+
338
+ self.brightness_slider.setValue(value)
339
+ self.brightness_edit.setText(f"{value}")
340
+
341
+ def get_contrast(self):
342
+ """Get current contrast value."""
343
+
344
+ return self.contrast_slider.value()
345
+
346
+ def set_contrast(self, value):
347
+ """Set contrast value."""
348
+
349
+ self.contrast_slider.setValue(value)
350
+ self.contrast_edit.setText(f"{value}")
351
+
352
+ def get_gamma(self):
353
+ """Get current gamma value."""
354
+
355
+ return self.gamma_slider.value()
356
+
357
+ def set_gamma(self, value):
358
+ """Set gamma value."""
359
+
360
+ self.gamma_slider.setValue(value)
361
+ self.gamma_edit.setText(f"{value / 100.0:.2f}")
362
+
363
+ def reset_to_defaults(self):
364
+ """Reset all adjustment values to their default states."""
365
+
366
+ self.set_annotation_size(10) # Default value
367
+ self.set_pan_speed(10) # Default value
368
+ self.set_join_threshold(2) # Default value
369
+ self.set_fragment_threshold(0) # Default value
370
+ self.set_brightness(0) # Default value
371
+ self.set_contrast(0) # Default value
372
+ self.set_gamma(100) # Default value (1.0)