PyImageLabeling 1.0.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.
- PyImageLabeling/__init__.py +22 -0
- PyImageLabeling/config.json +289 -0
- PyImageLabeling/controller/Controller.py +25 -0
- PyImageLabeling/controller/Events.py +147 -0
- PyImageLabeling/controller/FileEvents.py +69 -0
- PyImageLabeling/controller/ImageEvents.py +32 -0
- PyImageLabeling/controller/LabelEvents.py +219 -0
- PyImageLabeling/controller/LabelingEvents.py +123 -0
- PyImageLabeling/controller/settings/ContourFillinSetting.py +93 -0
- PyImageLabeling/controller/settings/CoutourFillingApplyCancel.py +37 -0
- PyImageLabeling/controller/settings/EraserSetting.py +73 -0
- PyImageLabeling/controller/settings/LabelSetting.py +91 -0
- PyImageLabeling/controller/settings/MagicPenSetting.py +125 -0
- PyImageLabeling/controller/settings/OpacitySetting.py +66 -0
- PyImageLabeling/controller/settings/PaintBrushSetting.py +66 -0
- PyImageLabeling/icons/apply.png +0 -0
- PyImageLabeling/icons/asterisk-green.png +0 -0
- PyImageLabeling/icons/asterisk-red.png +0 -0
- PyImageLabeling/icons/back.png +0 -0
- PyImageLabeling/icons/border.png +0 -0
- PyImageLabeling/icons/cancel.png +0 -0
- PyImageLabeling/icons/cleaner.png +0 -0
- PyImageLabeling/icons/close.png +0 -0
- PyImageLabeling/icons/down.png +0 -0
- PyImageLabeling/icons/ellipse.png +0 -0
- PyImageLabeling/icons/eraser.png +0 -0
- PyImageLabeling/icons/filling.png +0 -0
- PyImageLabeling/icons/logoMAIA.png +0 -0
- PyImageLabeling/icons/magic.png +0 -0
- PyImageLabeling/icons/maia.png +0 -0
- PyImageLabeling/icons/maia1.png +0 -0
- PyImageLabeling/icons/maia3.ico +0 -0
- PyImageLabeling/icons/maia_icon.png +0 -0
- PyImageLabeling/icons/move.png +0 -0
- PyImageLabeling/icons/opacity.png +0 -0
- PyImageLabeling/icons/open_image.png +0 -0
- PyImageLabeling/icons/open_layer.png +0 -0
- PyImageLabeling/icons/paint.png +0 -0
- PyImageLabeling/icons/plus.png +0 -0
- PyImageLabeling/icons/polygon.png +0 -0
- PyImageLabeling/icons/rectangle.png +0 -0
- PyImageLabeling/icons/reset.png +0 -0
- PyImageLabeling/icons/save.png +0 -0
- PyImageLabeling/icons/setting.png +0 -0
- PyImageLabeling/icons/transparency.png:Zone.Identifier +4 -0
- PyImageLabeling/icons/up.png +0 -0
- PyImageLabeling/icons/visibility.png +0 -0
- PyImageLabeling/icons/zoom_minus.png +0 -0
- PyImageLabeling/icons/zoom_plus.png +0 -0
- PyImageLabeling/model/Core.py +795 -0
- PyImageLabeling/model/File/Files.py +166 -0
- PyImageLabeling/model/File/NextImage.py +36 -0
- PyImageLabeling/model/File/PreviousImage.py +19 -0
- PyImageLabeling/model/Image/MoveImage.py +32 -0
- PyImageLabeling/model/Image/ResetMoveZoomImage.py +16 -0
- PyImageLabeling/model/Image/ZoomMinus.py +25 -0
- PyImageLabeling/model/Image/ZoomPlus.py +16 -0
- PyImageLabeling/model/Labeling/ClearAll.py +22 -0
- PyImageLabeling/model/Labeling/ContourFilling.py +135 -0
- PyImageLabeling/model/Labeling/Ellipse.py +350 -0
- PyImageLabeling/model/Labeling/Eraser.py +131 -0
- PyImageLabeling/model/Labeling/MagicPen.py +131 -0
- PyImageLabeling/model/Labeling/PaintBrush.py +207 -0
- PyImageLabeling/model/Labeling/Polygon.py +279 -0
- PyImageLabeling/model/Labeling/Rectangle.py +248 -0
- PyImageLabeling/model/Labeling/Undo.py +12 -0
- PyImageLabeling/model/Model.py +40 -0
- PyImageLabeling/model/Utils.py +40 -0
- PyImageLabeling/old_version/label_rectangle_properties.json +6 -0
- PyImageLabeling/old_version/main.py +2073 -0
- PyImageLabeling/old_version/models/EraseSettingsDialog.py +51 -0
- PyImageLabeling/old_version/models/LabeledRectangle.py +80 -0
- PyImageLabeling/old_version/models/MagicSettingsDialog.py +119 -0
- PyImageLabeling/old_version/models/OverlayOpacityDialog.py +63 -0
- PyImageLabeling/old_version/models/PaintSettingsDialog.py +289 -0
- PyImageLabeling/old_version/models/PointItem.py +66 -0
- PyImageLabeling/old_version/models/ProcessWorker.py +52 -0
- PyImageLabeling/old_version/models/ZoomableGraphicsView.py +1214 -0
- PyImageLabeling/old_version/models/tools/ContourTool.py +279 -0
- PyImageLabeling/old_version/models/tools/EraserTool.py +290 -0
- PyImageLabeling/old_version/models/tools/MagicPenTool.py +199 -0
- PyImageLabeling/old_version/models/tools/OverlayTool.py +179 -0
- PyImageLabeling/old_version/models/tools/PaintTool.py +68 -0
- PyImageLabeling/old_version/models/tools/PolygonTool.py +786 -0
- PyImageLabeling/old_version/models/tools/RectangleTool.py +1036 -0
- PyImageLabeling/parameters.json +1 -0
- PyImageLabeling/style.css +611 -0
- PyImageLabeling/view/Builder.py +333 -0
- PyImageLabeling/view/QBackgroundItem.py +30 -0
- PyImageLabeling/view/QWidgets.py +10 -0
- PyImageLabeling/view/View.py +226 -0
- PyImageLabeling/view/ZoomableGraphicsView.py +91 -0
- PyImageLabeling/view/__init__.py +0 -0
- pyimagelabeling-1.0.0.dist-info/METADATA +55 -0
- pyimagelabeling-1.0.0.dist-info/RECORD +99 -0
- pyimagelabeling-1.0.0.dist-info/WHEEL +5 -0
- pyimagelabeling-1.0.0.dist-info/licenses/LICENCE +22 -0
- pyimagelabeling-1.0.0.dist-info/top_level.txt +2 -0
- pypi/publish_pypi.py +18 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt, QPointF
|
|
2
|
+
from PyQt6.QtGui import QColor
|
|
3
|
+
from PyQt6.QtWidgets import QProgressDialog, QMessageBox, QApplication
|
|
4
|
+
from models.ProcessWorker import ProcessWorker
|
|
5
|
+
from models.PointItem import PointItem
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
class MagicPenTool:
|
|
9
|
+
def fill_shape(self, scene_pos):
|
|
10
|
+
"""Fill a shape with points using magic pen"""
|
|
11
|
+
if not self.base_pixmap:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
# Create progress dialog
|
|
15
|
+
progress = QProgressDialog("Processing magic pen fill...", "Cancel", 0, 0, self)
|
|
16
|
+
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
17
|
+
progress.show()
|
|
18
|
+
|
|
19
|
+
# Create worker thread for fill operation
|
|
20
|
+
self.worker = ProcessWorker(
|
|
21
|
+
self._fill_shape_worker,
|
|
22
|
+
args=[scene_pos],
|
|
23
|
+
timeout=self.process_timeout
|
|
24
|
+
)
|
|
25
|
+
self.worker.finished.connect(
|
|
26
|
+
lambda points: self._handle_fill_complete(points, progress)
|
|
27
|
+
)
|
|
28
|
+
self.worker.error.connect(
|
|
29
|
+
lambda error: self._handle_fill_error(error, progress)
|
|
30
|
+
)
|
|
31
|
+
self.worker.start()
|
|
32
|
+
|
|
33
|
+
def _fill_shape_worker(self, scene_pos):
|
|
34
|
+
"""Worker function to fill shape (runs in separate thread, with hard limits)"""
|
|
35
|
+
if not self.raw_image:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
image_x = int(scene_pos.x())
|
|
39
|
+
image_y = int(scene_pos.y())
|
|
40
|
+
|
|
41
|
+
width, height = self.raw_image.width(), self.raw_image.height()
|
|
42
|
+
|
|
43
|
+
if not (0 <= image_x < width and 0 <= image_y < height):
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
# Obtenir la couleur cible
|
|
47
|
+
target_color = QColor(self.raw_image.pixel(image_x, image_y))
|
|
48
|
+
target_hue = target_color.hue()
|
|
49
|
+
target_sat = target_color.saturation()
|
|
50
|
+
target_val = target_color.value()
|
|
51
|
+
tolerance = self.magic_pen_tolerance
|
|
52
|
+
|
|
53
|
+
points_to_create = []
|
|
54
|
+
visited = set()
|
|
55
|
+
start_time = time.time()
|
|
56
|
+
|
|
57
|
+
MAX_POINTS_LIMIT = self.max_points_limite
|
|
58
|
+
directions = [
|
|
59
|
+
(1, 0), (-1, 0), (0, 1), (0, -1),
|
|
60
|
+
(1, 1), (-1, -1), (1, -1), (-1, 1)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
from collections import deque
|
|
64
|
+
queue = deque([(image_x, image_y)])
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
while queue:
|
|
68
|
+
if time.time() - start_time > self.process_timeout:
|
|
69
|
+
print(f"Timeout atteint ({self.process_timeout}s). Annulation du remplissage.")
|
|
70
|
+
return [] # Retourne une liste vide → pas de remplissage
|
|
71
|
+
|
|
72
|
+
if len(points_to_create) >= MAX_POINTS_LIMIT:
|
|
73
|
+
print(f"Trop de points ({MAX_POINTS_LIMIT}). Annulation du remplissage.")
|
|
74
|
+
return [] # Retourne une liste vide → pas de remplissage
|
|
75
|
+
|
|
76
|
+
x, y = queue.popleft()
|
|
77
|
+
if (x, y) in visited:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
visited.add((x, y))
|
|
81
|
+
|
|
82
|
+
if not (0 <= x < width and 0 <= y < height):
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Vérification de la couleur avec la tolérance
|
|
86
|
+
current_color = QColor(self.raw_image.pixel(x, y))
|
|
87
|
+
current_hue = current_color.hue()
|
|
88
|
+
current_sat = current_color.saturation()
|
|
89
|
+
current_val = current_color.value()
|
|
90
|
+
|
|
91
|
+
if target_hue == -1 or current_hue == -1:
|
|
92
|
+
if abs(current_val - target_val) > tolerance:
|
|
93
|
+
continue
|
|
94
|
+
else:
|
|
95
|
+
hue_diff = min(abs(current_hue - target_hue),
|
|
96
|
+
360 - abs(current_hue - target_hue))
|
|
97
|
+
|
|
98
|
+
if (hue_diff > tolerance or
|
|
99
|
+
abs(current_sat - target_sat) > tolerance or
|
|
100
|
+
abs(current_val - target_val) > tolerance):
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Ajouter le point
|
|
104
|
+
points_to_create.append((x, y))
|
|
105
|
+
|
|
106
|
+
# Ajouter les voisins
|
|
107
|
+
for dx, dy in directions:
|
|
108
|
+
new_x, new_y = x + dx, y + dy
|
|
109
|
+
if (new_x, new_y) not in visited:
|
|
110
|
+
queue.append((new_x, new_y))
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"Erreur pendant le remplissage : {e}")
|
|
114
|
+
|
|
115
|
+
return points_to_create
|
|
116
|
+
|
|
117
|
+
def _handle_fill_complete(self, point_coords, progress):
|
|
118
|
+
"""Handle completion of fill operation"""
|
|
119
|
+
if not progress:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
progress.close()
|
|
123
|
+
|
|
124
|
+
if not point_coords:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Prepare for UI update and point creation
|
|
128
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Capture point parameters IMMEDIATELY at the time of fill
|
|
132
|
+
current_point_radius = self.point_radius
|
|
133
|
+
current_point_color = self.point_color
|
|
134
|
+
current_point_opacity = self.point_opacity
|
|
135
|
+
current_point_label = self.point_label
|
|
136
|
+
|
|
137
|
+
# Create a batch of points
|
|
138
|
+
chunk_size = 5000 # Process in larger batches for efficiency
|
|
139
|
+
all_new_points = []
|
|
140
|
+
|
|
141
|
+
for i in range(0, len(point_coords), chunk_size):
|
|
142
|
+
chunk = point_coords[i:i + chunk_size]
|
|
143
|
+
batch_points = []
|
|
144
|
+
|
|
145
|
+
# Update progress dialog
|
|
146
|
+
if progress and not progress.wasCanceled():
|
|
147
|
+
progress.setValue(i)
|
|
148
|
+
progress.setMaximum(len(point_coords))
|
|
149
|
+
progress.setLabelText(f"Creating points: {i}/{len(point_coords)}")
|
|
150
|
+
QApplication.processEvents()
|
|
151
|
+
|
|
152
|
+
for x, y in chunk:
|
|
153
|
+
try:
|
|
154
|
+
point_item = PointItem(
|
|
155
|
+
current_point_label,
|
|
156
|
+
x, y,
|
|
157
|
+
current_point_radius, # Use captured radius
|
|
158
|
+
current_point_color, # Use captured color
|
|
159
|
+
current_point_opacity # Use captured opacity
|
|
160
|
+
)
|
|
161
|
+
self.scene.addItem(point_item)
|
|
162
|
+
self.points.append(point_item)
|
|
163
|
+
batch_points.append(point_item)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"Error creating point at ({x}, {y}): {e}")
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
all_new_points.extend(batch_points)
|
|
169
|
+
|
|
170
|
+
# Allow UI to update between chunks
|
|
171
|
+
QApplication.processEvents()
|
|
172
|
+
|
|
173
|
+
# Store for undo history
|
|
174
|
+
if all_new_points:
|
|
175
|
+
self.points_history.append(all_new_points)
|
|
176
|
+
|
|
177
|
+
# Update the points overlay
|
|
178
|
+
self.update_points_overlay()
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"Error during fill completion: {e}")
|
|
181
|
+
finally:
|
|
182
|
+
QApplication.restoreOverrideCursor()
|
|
183
|
+
|
|
184
|
+
def _handle_fill_error(self, error, progress):
|
|
185
|
+
"""Handle errors during fill operation"""
|
|
186
|
+
if progress:
|
|
187
|
+
progress.close()
|
|
188
|
+
QMessageBox.warning(self, "Error", f"Magic pen fill operation failed: {error}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def toggle_magic_pen(self, enabled):
|
|
192
|
+
"""Toggle magic pen mode on/off"""
|
|
193
|
+
self.magic_pen_mode = enabled
|
|
194
|
+
if enabled:
|
|
195
|
+
self.paint_mode = False
|
|
196
|
+
self.erase_mode = False
|
|
197
|
+
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
198
|
+
else:
|
|
199
|
+
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt, QRectF
|
|
2
|
+
from PyQt6.QtGui import QPixmap, QBrush, QPainter, QColor
|
|
3
|
+
from models.OverlayOpacityDialog import OverlayOpacityDialog
|
|
4
|
+
|
|
5
|
+
class OverlayTool:
|
|
6
|
+
def add_overlay(self, overlay_pixmap):
|
|
7
|
+
"""
|
|
8
|
+
Add an overlay layer on top of the base image.
|
|
9
|
+
|
|
10
|
+
Parameters:
|
|
11
|
+
overlay_pixmap (QPixmap): The overlay image to be added
|
|
12
|
+
"""
|
|
13
|
+
# Remove any existing overlay first
|
|
14
|
+
self.remove_overlay()
|
|
15
|
+
|
|
16
|
+
# Scale the overlay to match the base image size if needed
|
|
17
|
+
if self.base_pixmap and overlay_pixmap.size() != self.base_pixmap.size():
|
|
18
|
+
overlay_pixmap = overlay_pixmap.scaled(
|
|
19
|
+
self.base_pixmap.size(),
|
|
20
|
+
Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
21
|
+
Qt.TransformationMode.SmoothTransformation
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Create a pixmap item for the overlay and add it to the scene
|
|
25
|
+
self.overlay_pixmap_item = self.scene.addPixmap(overlay_pixmap)
|
|
26
|
+
|
|
27
|
+
# Position the overlay to align with the base image
|
|
28
|
+
if self.pixmap_item:
|
|
29
|
+
self.overlay_pixmap_item.setPos(self.pixmap_item.pos())
|
|
30
|
+
|
|
31
|
+
# Set the overlay above the base image
|
|
32
|
+
self.overlay_pixmap_item.setZValue(1) # Base image is typically at Z=0
|
|
33
|
+
|
|
34
|
+
# Set default overlay opacity (semi-transparent)
|
|
35
|
+
self.overlay_opacity = 128 # 0-255 range, 128 is 50% opacity
|
|
36
|
+
self.overlay_pixmap_item.setOpacity(self.overlay_opacity / 255.0)
|
|
37
|
+
|
|
38
|
+
# Store a reference to the original overlay pixmap
|
|
39
|
+
self.overlay_original_pixmap = overlay_pixmap
|
|
40
|
+
|
|
41
|
+
# Update the scene
|
|
42
|
+
self.scene.update()
|
|
43
|
+
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def remove_overlay(self):
|
|
47
|
+
"""Remove overlay if exists"""
|
|
48
|
+
if self.overlay_pixmap_item:
|
|
49
|
+
self.scene.removeItem(self.overlay_pixmap_item)
|
|
50
|
+
self.overlay_pixmap_item = None
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def show_opacity_dialog(self):
|
|
55
|
+
"""Show or create the opacity dialog"""
|
|
56
|
+
# Only show if there's an overlay or points
|
|
57
|
+
if not (self.overlay_pixmap_item or self.points):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Always create a fresh dialog with the current opacity value
|
|
61
|
+
# This ensures the dialog always shows the current value
|
|
62
|
+
current_opacity = self.points_opacity
|
|
63
|
+
|
|
64
|
+
# Close any existing dialog
|
|
65
|
+
if hasattr(self, "opacity_dialog") and self.opacity_dialog:
|
|
66
|
+
self.opacity_dialog.close()
|
|
67
|
+
|
|
68
|
+
# Create new dialog with current opacity value
|
|
69
|
+
self.opacity_dialog = OverlayOpacityDialog(self, current_opacity)
|
|
70
|
+
self.opacity_dialog.show()
|
|
71
|
+
|
|
72
|
+
def update_overlay_opacity(self, value):
|
|
73
|
+
"""Updates the opacity for all points and overlay with consistent rendering for overlapping points"""
|
|
74
|
+
|
|
75
|
+
self.points_opacity = value # Store the opacity value
|
|
76
|
+
|
|
77
|
+
# If there are no points, just update the overlay opacity if it exists
|
|
78
|
+
if not self.points:
|
|
79
|
+
if self.overlay_pixmap_item:
|
|
80
|
+
self.overlay_opacity = value
|
|
81
|
+
self.overlay_pixmap_item.setOpacity(value / 255.0)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Update the unified point overlay
|
|
85
|
+
self.update_points_overlay()
|
|
86
|
+
|
|
87
|
+
def update_points_overlay(self):
|
|
88
|
+
"""Creates or updates the unified points overlay using batched rendering with improved synchronization"""
|
|
89
|
+
# Skip if no base pixmap
|
|
90
|
+
if not self.base_pixmap:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Get the base image dimensions
|
|
94
|
+
width = self.base_pixmap.width()
|
|
95
|
+
height = self.base_pixmap.height()
|
|
96
|
+
|
|
97
|
+
# Create a transparent pixmap for the points if it doesn't exist already
|
|
98
|
+
if not hasattr(self, 'points_pixmap') or self.points_pixmap is None:
|
|
99
|
+
self.points_pixmap = QPixmap(width, height)
|
|
100
|
+
self.points_pixmap.fill(Qt.GlobalColor.transparent)
|
|
101
|
+
self.last_rendered_points_count = 0
|
|
102
|
+
|
|
103
|
+
# Handle undo operations - rebuild the pixmap if points were removed
|
|
104
|
+
if hasattr(self, 'last_rendered_points_count') and self.last_rendered_points_count > len(self.points):
|
|
105
|
+
# Reset the pixmap and force full redraw
|
|
106
|
+
self.points_pixmap.fill(Qt.GlobalColor.transparent)
|
|
107
|
+
self.last_rendered_points_count = 0
|
|
108
|
+
|
|
109
|
+
# Always render all points when last_rendered_points_count is 0
|
|
110
|
+
if self.last_rendered_points_count == 0:
|
|
111
|
+
points_to_render = self.points
|
|
112
|
+
else:
|
|
113
|
+
# Only render the new points that haven't been rendered yet
|
|
114
|
+
points_to_render = self.points[self.last_rendered_points_count:]
|
|
115
|
+
|
|
116
|
+
# Create a painter even if there are no new points to render
|
|
117
|
+
# This ensures the pixmap is properly updated when undoing to empty state
|
|
118
|
+
painter = QPainter(self.points_pixmap)
|
|
119
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
120
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
|
|
121
|
+
|
|
122
|
+
# Store the last used color and brush to avoid unnecessary brush changes
|
|
123
|
+
last_color = None
|
|
124
|
+
|
|
125
|
+
# Draw only the new points onto the unified pixmap
|
|
126
|
+
for point_item in points_to_render:
|
|
127
|
+
pos = point_item.get_position()
|
|
128
|
+
radius = point_item._fixed_radius # Use the stored radius
|
|
129
|
+
|
|
130
|
+
# Get the point's individual color (if points can have different colors)
|
|
131
|
+
if hasattr(point_item, '_color'):
|
|
132
|
+
current_color = point_item._color
|
|
133
|
+
else:
|
|
134
|
+
current_color = self.point_color
|
|
135
|
+
|
|
136
|
+
# Only change the brush if the color changes
|
|
137
|
+
if current_color != last_color:
|
|
138
|
+
color = QColor(current_color)
|
|
139
|
+
painter.setBrush(QBrush(color))
|
|
140
|
+
painter.setPen(Qt.PenStyle.NoPen) # No outline, just filled circles
|
|
141
|
+
last_color = current_color
|
|
142
|
+
|
|
143
|
+
painter.drawEllipse(pos, radius, radius)
|
|
144
|
+
|
|
145
|
+
# Ensure point is properly hidden
|
|
146
|
+
point_item.setVisible(False)
|
|
147
|
+
|
|
148
|
+
painter.end()
|
|
149
|
+
|
|
150
|
+
# Update our count of rendered points
|
|
151
|
+
self.last_rendered_points_count = len(self.points)
|
|
152
|
+
|
|
153
|
+
# Create or update the overlay item rather than removing and recreating
|
|
154
|
+
if not hasattr(self, 'points_overlay_item') or self.points_overlay_item is None:
|
|
155
|
+
# Create new overlay item with the unified points
|
|
156
|
+
self.points_overlay_item = self.scene.addPixmap(self.points_pixmap)
|
|
157
|
+
self.points_overlay_item.setZValue(1) # Above base image, below any other overlays
|
|
158
|
+
else:
|
|
159
|
+
# Update existing overlay with new pixmap
|
|
160
|
+
self.points_overlay_item.setPixmap(self.points_pixmap)
|
|
161
|
+
|
|
162
|
+
# Apply current opacity
|
|
163
|
+
self.points_overlay_item.setOpacity(self.points_opacity / 255.0)
|
|
164
|
+
|
|
165
|
+
# Calculate the region that needs updating
|
|
166
|
+
update_rect = QRectF()
|
|
167
|
+
for point in points_to_render:
|
|
168
|
+
pos = point.get_position()
|
|
169
|
+
r = point._fixed_radius
|
|
170
|
+
point_rect = QRectF(pos.x() - r, pos.y() - r, r * 2, r * 2)
|
|
171
|
+
if update_rect.isEmpty():
|
|
172
|
+
update_rect = point_rect
|
|
173
|
+
else:
|
|
174
|
+
update_rect = update_rect.united(point_rect)
|
|
175
|
+
|
|
176
|
+
# Add a small margin and update only the changed area
|
|
177
|
+
if not update_rect.isEmpty():
|
|
178
|
+
update_rect.adjust(-2, -2, 2, 2)
|
|
179
|
+
self.scene.update(update_rect)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt
|
|
2
|
+
from models.PointItem import PointItem
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
class PaintTool:
|
|
6
|
+
def add_point(self, scene_pos):
|
|
7
|
+
current_radius = self.point_radius
|
|
8
|
+
current_color = self.point_color
|
|
9
|
+
current_opacity = self.point_opacity
|
|
10
|
+
current_label = self.point_label
|
|
11
|
+
point_item = PointItem(
|
|
12
|
+
current_label,
|
|
13
|
+
scene_pos.x(), scene_pos.y(),
|
|
14
|
+
current_radius,
|
|
15
|
+
current_color,
|
|
16
|
+
current_opacity
|
|
17
|
+
)
|
|
18
|
+
self.scene.addItem(point_item)
|
|
19
|
+
point_item.setVisible(False)
|
|
20
|
+
self.points.append(point_item)
|
|
21
|
+
self.current_stroke.append(point_item)
|
|
22
|
+
self.update_points_overlay()
|
|
23
|
+
|
|
24
|
+
def draw_continuous_line(self, start_pos, end_pos):
|
|
25
|
+
if not start_pos or not end_pos:
|
|
26
|
+
return
|
|
27
|
+
current_point_radius = self.point_radius
|
|
28
|
+
current_point_color = self.point_color
|
|
29
|
+
current_point_opacity = self.point_opacity
|
|
30
|
+
current_point_label = self.point_label
|
|
31
|
+
distance = ((end_pos.x() - start_pos.x()) ** 2 + (end_pos.y() - start_pos.y()) ** 2) ** 0.5
|
|
32
|
+
num_steps = max(int(distance * 2), 1)
|
|
33
|
+
t_values = np.linspace(0, 1, num_steps + 1)
|
|
34
|
+
for t in t_values:
|
|
35
|
+
x = start_pos.x() + t * (end_pos.x() - start_pos.x())
|
|
36
|
+
y = start_pos.y() + t * (end_pos.y() - start_pos.y())
|
|
37
|
+
point_item = PointItem(
|
|
38
|
+
current_point_label,
|
|
39
|
+
x, y,
|
|
40
|
+
current_point_radius,
|
|
41
|
+
current_point_color,
|
|
42
|
+
current_point_opacity
|
|
43
|
+
)
|
|
44
|
+
self.scene.addItem(point_item)
|
|
45
|
+
point_item.setVisible(self.drawn_points_visible)
|
|
46
|
+
self.points.append(point_item)
|
|
47
|
+
self.current_stroke.append(point_item)
|
|
48
|
+
self.update_points_overlay()
|
|
49
|
+
|
|
50
|
+
def toggle_paint_mode(self, enabled):
|
|
51
|
+
self.paint_mode = enabled
|
|
52
|
+
if enabled:
|
|
53
|
+
self.erase_mode = False
|
|
54
|
+
self.magic_pen_mode = False
|
|
55
|
+
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
56
|
+
self.setDragMode(self.DragMode.NoDrag)
|
|
57
|
+
else:
|
|
58
|
+
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
59
|
+
self.setDragMode(self.DragMode.ScrollHandDrag)
|
|
60
|
+
|
|
61
|
+
def clear_points(self):
|
|
62
|
+
self.points_history.append(self.points[:])
|
|
63
|
+
self.points = []
|
|
64
|
+
if hasattr(self, 'points_overlay_item') and self.points_overlay_item:
|
|
65
|
+
self.scene.removeItem(self.points_overlay_item)
|
|
66
|
+
self.points_overlay_item = None
|
|
67
|
+
self.points_pixmap = None
|
|
68
|
+
self.last_rendered_points_count = 0
|