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,51 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from PyQt6.QtWidgets import (
|
|
7
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
8
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu
|
|
9
|
+
)
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
11
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
12
|
+
import gc
|
|
13
|
+
import math
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
class EraseSettingsDialog(QDialog):
|
|
17
|
+
def __init__(self, parent=None, current_eraser_size=None, absolute_mode=False):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.setWindowTitle("Eraser Settings")
|
|
20
|
+
self.eraser_size = current_eraser_size or 10
|
|
21
|
+
self.absolute_mode = absolute_mode
|
|
22
|
+
|
|
23
|
+
layout = QFormLayout()
|
|
24
|
+
|
|
25
|
+
# Eraser size slider
|
|
26
|
+
self.eraser_slider = QSlider(Qt.Orientation.Horizontal)
|
|
27
|
+
self.eraser_slider.setRange(1, 100)
|
|
28
|
+
self.eraser_slider.setValue(self.eraser_size)
|
|
29
|
+
layout.addRow("Eraser Size:", self.eraser_slider)
|
|
30
|
+
self.eraser_spinbox = QSpinBox()
|
|
31
|
+
|
|
32
|
+
self.eraser_spinbox.setRange(1, 100)
|
|
33
|
+
self.eraser_spinbox.setValue(self.eraser_size)
|
|
34
|
+
self.eraser_spinbox.valueChanged.connect(self.eraser_slider.setValue)
|
|
35
|
+
self.eraser_slider.valueChanged.connect(self.eraser_spinbox.setValue)
|
|
36
|
+
layout.addRow("", self.eraser_spinbox)
|
|
37
|
+
|
|
38
|
+
# Add absolute mode checkbox
|
|
39
|
+
self.absolute_checkbox = QCheckBox("Absolute Mode")
|
|
40
|
+
self.absolute_checkbox.setChecked(self.absolute_mode)
|
|
41
|
+
layout.addRow("", self.absolute_checkbox)
|
|
42
|
+
|
|
43
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
44
|
+
self.buttons.accepted.connect(self.accept)
|
|
45
|
+
self.buttons.rejected.connect(self.reject)
|
|
46
|
+
layout.addRow(self.buttons)
|
|
47
|
+
|
|
48
|
+
self.setLayout(layout)
|
|
49
|
+
|
|
50
|
+
def get_settings(self):
|
|
51
|
+
return self.eraser_slider.value(), self.absolute_checkbox.isChecked()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
import cv2
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
9
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu, QGraphicsTextItem
|
|
10
|
+
)
|
|
11
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
12
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
13
|
+
import gc
|
|
14
|
+
import math
|
|
15
|
+
import traceback
|
|
16
|
+
|
|
17
|
+
class LabeledRectangle(QGraphicsRectItem):
|
|
18
|
+
def __init__(self, x, y, width, height, label="", color = None):
|
|
19
|
+
super().__init__(x, y, width, height)
|
|
20
|
+
self.label = label
|
|
21
|
+
self.color = color if color else QColor(255, 0, 0) # Red outline
|
|
22
|
+
|
|
23
|
+
self.setPen(QPen(QColor(self.color), 2))
|
|
24
|
+
|
|
25
|
+
# Create label text item
|
|
26
|
+
self.text_item = QGraphicsTextItem(self.label, self)
|
|
27
|
+
self.text_item.setDefaultTextColor(QColor(255, 255, 255))
|
|
28
|
+
self.text_item.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
|
29
|
+
|
|
30
|
+
# Position the text at the top-left corner of the rectangle
|
|
31
|
+
self.text_item.setPos(x, y - 20) # Slightly above the rectangle
|
|
32
|
+
|
|
33
|
+
# Make the rectangle selectable and movable
|
|
34
|
+
self.setFlag(QGraphicsRectItem.GraphicsItemFlag.ItemIsSelectable, True)
|
|
35
|
+
self.setFlag(QGraphicsRectItem.GraphicsItemFlag.ItemIsMovable, True)
|
|
36
|
+
|
|
37
|
+
def set_color(self, color):
|
|
38
|
+
"""Update the rectangle color"""
|
|
39
|
+
self.color = color
|
|
40
|
+
self.setPen(QPen(QColor(self.color), 2))
|
|
41
|
+
|
|
42
|
+
def set_label(self, label):
|
|
43
|
+
"""Update the rectangle label"""
|
|
44
|
+
self.label = label
|
|
45
|
+
if hasattr(self, 'text_item'):
|
|
46
|
+
self.text_item.setPlainText(label)
|
|
47
|
+
|
|
48
|
+
def set_thickness(self, thickness):
|
|
49
|
+
"""Update the rectangle thickness"""
|
|
50
|
+
self.thickness = thickness
|
|
51
|
+
self.setPen(QPen(QColor(self.color), self.thickness))
|
|
52
|
+
|
|
53
|
+
def get_color(self):
|
|
54
|
+
"""Get the current color"""
|
|
55
|
+
return self.color
|
|
56
|
+
|
|
57
|
+
def get_label(self):
|
|
58
|
+
"""Get the current label"""
|
|
59
|
+
return self.label
|
|
60
|
+
|
|
61
|
+
def boundingRect(self):
|
|
62
|
+
"""Return the bounding rectangle including the text"""
|
|
63
|
+
rect = super().boundingRect()
|
|
64
|
+
if hasattr(self, 'text_item'):
|
|
65
|
+
text_rect = self.text_item.boundingRect()
|
|
66
|
+
text_rect.translate(self.text_item.pos())
|
|
67
|
+
rect = rect.united(text_rect)
|
|
68
|
+
return rect
|
|
69
|
+
|
|
70
|
+
def contains(self, point):
|
|
71
|
+
"""Check if point is within the rectangle (for click detection)"""
|
|
72
|
+
return super().contains(point)
|
|
73
|
+
|
|
74
|
+
def itemChange(self, change, value):
|
|
75
|
+
"""Handle item changes (like position changes)"""
|
|
76
|
+
if change == QGraphicsRectItem.GraphicsItemChange.ItemPositionChange and hasattr(self, 'text_item'):
|
|
77
|
+
# Update text position when rectangle moves
|
|
78
|
+
new_pos = value
|
|
79
|
+
self.text_item.setPos(new_pos.x(), new_pos.y() - 20)
|
|
80
|
+
return super().itemChange(change, value)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from PyQt6.QtWidgets import (
|
|
7
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
8
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu
|
|
9
|
+
)
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
11
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
12
|
+
import gc
|
|
13
|
+
import math
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
class MagicSettingsDialog(QDialog):
|
|
17
|
+
def __init__(self, parent=None, current_tolerance=20, current_timeout=5, current_max_points=100000):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.setWindowTitle("Magic Pen Settings")
|
|
20
|
+
self.tolerance = current_tolerance
|
|
21
|
+
self.max_point = current_max_points
|
|
22
|
+
layout = QVBoxLayout()
|
|
23
|
+
|
|
24
|
+
# Create a form layout for the controls
|
|
25
|
+
form_layout = QFormLayout()
|
|
26
|
+
|
|
27
|
+
# Tolerance slider
|
|
28
|
+
self.tolerance_slider = QSlider(Qt.Orientation.Horizontal)
|
|
29
|
+
self.tolerance_slider.setRange(0, 100)
|
|
30
|
+
self.tolerance_slider.setValue(self.tolerance)
|
|
31
|
+
self.tolerance_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
32
|
+
self.tolerance_slider.setTickInterval(10)
|
|
33
|
+
form_layout.addRow("Tolerance:", self.tolerance_slider)
|
|
34
|
+
|
|
35
|
+
# Tolerance spinbox
|
|
36
|
+
self.tolerance_spinbox = QSpinBox()
|
|
37
|
+
self.tolerance_spinbox.setRange(0, 100)
|
|
38
|
+
self.tolerance_spinbox.setValue(self.tolerance)
|
|
39
|
+
|
|
40
|
+
# Connect slider and spinbox
|
|
41
|
+
self.tolerance_spinbox.valueChanged.connect(self.tolerance_slider.setValue)
|
|
42
|
+
self.tolerance_slider.valueChanged.connect(self.tolerance_spinbox.setValue)
|
|
43
|
+
|
|
44
|
+
form_layout.addRow("Value:", self.tolerance_spinbox)
|
|
45
|
+
|
|
46
|
+
# Add a description of what different tolerance values mean
|
|
47
|
+
tolerance_help = QLabel("Lower values = more precise color matching\nHigher values = more inclusive fill")
|
|
48
|
+
tolerance_help.setStyleSheet("color: #666; font-style: italic;")
|
|
49
|
+
form_layout.addRow("", tolerance_help)
|
|
50
|
+
|
|
51
|
+
# Add form layout to main layout
|
|
52
|
+
layout.addLayout(form_layout)
|
|
53
|
+
|
|
54
|
+
# Add timeout setting
|
|
55
|
+
timeout_layout = QHBoxLayout()
|
|
56
|
+
timeout_label = QLabel("Timeout (seconds):")
|
|
57
|
+
self.timeout_spinbox = QSpinBox()
|
|
58
|
+
self.timeout_spinbox.setRange(1, 60)
|
|
59
|
+
self.timeout_spinbox.setValue(current_timeout)
|
|
60
|
+
timeout_layout.addWidget(timeout_label)
|
|
61
|
+
timeout_layout.addWidget(self.timeout_spinbox)
|
|
62
|
+
layout.addLayout(timeout_layout)
|
|
63
|
+
|
|
64
|
+
# Add MAX_POINTS_LIMIT setting
|
|
65
|
+
layout.addSpacing(10)
|
|
66
|
+
points_limit_layout = QVBoxLayout()
|
|
67
|
+
|
|
68
|
+
points_limit_label = QLabel("Maximum Points Limit:")
|
|
69
|
+
points_limit_layout.addWidget(points_limit_label)
|
|
70
|
+
|
|
71
|
+
points_slider_layout = QHBoxLayout()
|
|
72
|
+
|
|
73
|
+
# Points limit slider
|
|
74
|
+
self.points_limit_slider = QSlider(Qt.Orientation.Horizontal)
|
|
75
|
+
self.points_limit_slider.setRange(5000, 500000)
|
|
76
|
+
self.points_limit_slider.setValue(self.max_point)
|
|
77
|
+
self.points_limit_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
78
|
+
self.points_limit_slider.setTickInterval(50000)
|
|
79
|
+
|
|
80
|
+
# Points limit spinbox
|
|
81
|
+
self.points_limit_spinbox = QSpinBox()
|
|
82
|
+
self.points_limit_spinbox.setRange(5000, 500000)
|
|
83
|
+
self.points_limit_spinbox.setValue(self.max_point)
|
|
84
|
+
self.points_limit_spinbox.setSingleStep(5000)
|
|
85
|
+
|
|
86
|
+
# Connect slider and spinbox
|
|
87
|
+
self.points_limit_spinbox.valueChanged.connect(self.points_limit_slider.setValue)
|
|
88
|
+
self.points_limit_slider.valueChanged.connect(self.points_limit_spinbox.setValue)
|
|
89
|
+
|
|
90
|
+
points_slider_layout.addWidget(self.points_limit_slider)
|
|
91
|
+
points_slider_layout.addWidget(self.points_limit_spinbox)
|
|
92
|
+
points_limit_layout.addLayout(points_slider_layout)
|
|
93
|
+
|
|
94
|
+
# Add description for points limit
|
|
95
|
+
points_limit_help = QLabel("Higher values allow filling larger areas but may use more memory")
|
|
96
|
+
points_limit_help.setStyleSheet("color: #666; font-style: italic;")
|
|
97
|
+
points_limit_layout.addWidget(points_limit_help)
|
|
98
|
+
|
|
99
|
+
layout.addLayout(points_limit_layout)
|
|
100
|
+
|
|
101
|
+
# Add spacer
|
|
102
|
+
layout.addSpacing(10)
|
|
103
|
+
|
|
104
|
+
# Add buttons
|
|
105
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
106
|
+
self.buttons.accepted.connect(self.accept)
|
|
107
|
+
self.buttons.rejected.connect(self.reject)
|
|
108
|
+
layout.addWidget(self.buttons)
|
|
109
|
+
|
|
110
|
+
self.setLayout(layout)
|
|
111
|
+
|
|
112
|
+
def get_tolerance(self):
|
|
113
|
+
return self.tolerance_slider.value()
|
|
114
|
+
|
|
115
|
+
def get_timeout(self):
|
|
116
|
+
return self.timeout_spinbox.value()
|
|
117
|
+
|
|
118
|
+
def get_max_points_limit(self):
|
|
119
|
+
return self.points_limit_spinbox.value()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from PyQt6.QtWidgets import (
|
|
7
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
8
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu
|
|
9
|
+
)
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
11
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
12
|
+
import gc
|
|
13
|
+
import math
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
class OverlayOpacityDialog(QDialog):
|
|
17
|
+
def __init__(self, parent=None, current_opacity=255):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.setWindowTitle("Overlay Opacity")
|
|
20
|
+
|
|
21
|
+
layout = QFormLayout()
|
|
22
|
+
|
|
23
|
+
# Opacity slider
|
|
24
|
+
self.slider = QSlider(Qt.Orientation.Horizontal)
|
|
25
|
+
self.slider.setRange(0, 255)
|
|
26
|
+
self.slider.setValue(current_opacity)
|
|
27
|
+
self.slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
28
|
+
self.slider.setTickInterval(25)
|
|
29
|
+
layout.addRow("Opacity:", self.slider)
|
|
30
|
+
|
|
31
|
+
# Opacity spinbox
|
|
32
|
+
self.opacity_spinbox = QSpinBox()
|
|
33
|
+
self.opacity_spinbox.setRange(0, 255)
|
|
34
|
+
self.opacity_spinbox.setValue(current_opacity)
|
|
35
|
+
layout.addRow("", self.opacity_spinbox)
|
|
36
|
+
|
|
37
|
+
# Sync slider and spinbox
|
|
38
|
+
self.opacity_spinbox.valueChanged.connect(self.slider.setValue)
|
|
39
|
+
self.slider.valueChanged.connect(self.opacity_spinbox.setValue)
|
|
40
|
+
|
|
41
|
+
# Preset buttons layout
|
|
42
|
+
preset_layout = QHBoxLayout()
|
|
43
|
+
presets = [(25, "25%"), (50, "50%"), (75, "75%"), (100, "100%")]
|
|
44
|
+
for value, label in presets:
|
|
45
|
+
btn = QPushButton(label)
|
|
46
|
+
btn.setFixedHeight(25)
|
|
47
|
+
btn.setProperty("class", "preset")
|
|
48
|
+
btn.clicked.connect(lambda checked, v=value: self.set_preset(v))
|
|
49
|
+
preset_layout.addWidget(btn)
|
|
50
|
+
layout.addRow("", preset_layout)
|
|
51
|
+
|
|
52
|
+
# Buttons
|
|
53
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
54
|
+
self.buttons.accepted.connect(self.accept)
|
|
55
|
+
self.buttons.rejected.connect(self.reject)
|
|
56
|
+
layout.addRow(self.buttons)
|
|
57
|
+
|
|
58
|
+
self.setLayout(layout)
|
|
59
|
+
|
|
60
|
+
def set_preset(self, percentage):
|
|
61
|
+
"""Set opacity to a preset percentage value"""
|
|
62
|
+
value = int((percentage / 100) * 255)
|
|
63
|
+
self.slider.setValue(value)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from PyQt6.QtWidgets import (
|
|
7
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
8
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu, QLineEdit
|
|
9
|
+
)
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
11
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
12
|
+
import gc
|
|
13
|
+
import math
|
|
14
|
+
import traceback
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
class LabelPaintPropertiesDialog(QDialog):
|
|
18
|
+
def __init__(self, parent=None):
|
|
19
|
+
super().__init__(parent)
|
|
20
|
+
self.setWindowTitle("Label Properties")
|
|
21
|
+
self.setMinimumWidth(300)
|
|
22
|
+
self.setStyleSheet("""
|
|
23
|
+
QDialog {
|
|
24
|
+
background-color: #000000;
|
|
25
|
+
color: white;
|
|
26
|
+
border: 1px solid #444444;
|
|
27
|
+
}
|
|
28
|
+
QLabel {
|
|
29
|
+
color: white;
|
|
30
|
+
font-size: 14px;
|
|
31
|
+
}
|
|
32
|
+
""")
|
|
33
|
+
|
|
34
|
+
layout = QVBoxLayout()
|
|
35
|
+
|
|
36
|
+
self.label_name = QLabel("Label: ")
|
|
37
|
+
self.label_color = QLabel("Color: ")
|
|
38
|
+
self.label_radius = QLabel("Radius: ")
|
|
39
|
+
self.label_opacity = QLabel("Opacity: ")
|
|
40
|
+
|
|
41
|
+
layout.addWidget(self.label_name)
|
|
42
|
+
layout.addWidget(self.label_color)
|
|
43
|
+
layout.addWidget(self.label_radius)
|
|
44
|
+
layout.addWidget(self.label_opacity)
|
|
45
|
+
|
|
46
|
+
self.setLayout(layout)
|
|
47
|
+
|
|
48
|
+
def update_properties(self, label, color, radius, opacity):
|
|
49
|
+
self.label_name.setText(f"Label: {label}")
|
|
50
|
+
self.label_color.setText(f"Color: {color.name()}")
|
|
51
|
+
self.label_radius.setText(f"Radius: {radius}")
|
|
52
|
+
self.label_opacity.setText(f"Opacity: {opacity}")
|
|
53
|
+
|
|
54
|
+
class LabelPropertiesManager:
|
|
55
|
+
"""Manages saving and loading of label properties"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, properties_file="label_paint_properties.json"):
|
|
58
|
+
self.properties_file = properties_file
|
|
59
|
+
self.label_properties = {}
|
|
60
|
+
self.load_properties()
|
|
61
|
+
|
|
62
|
+
def save_properties(self):
|
|
63
|
+
"""Save label properties to JSON file"""
|
|
64
|
+
try:
|
|
65
|
+
# Convert QColor objects to serializable format
|
|
66
|
+
serializable_props = {}
|
|
67
|
+
for label, props in self.label_properties.items():
|
|
68
|
+
color = props['color']
|
|
69
|
+
if isinstance(color, str):
|
|
70
|
+
color = QColor(color) # Convert string to QColor if needed
|
|
71
|
+
serializable_props[label] = {
|
|
72
|
+
'color': color.name(), # Safely get hex color
|
|
73
|
+
'radius': props['radius'],
|
|
74
|
+
'opacity': props['opacity']
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
with open(self.properties_file, 'w') as f:
|
|
78
|
+
json.dump(serializable_props, f, indent=2)
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"Error saving label properties: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def load_properties(self):
|
|
85
|
+
"""Load label properties from JSON file"""
|
|
86
|
+
try:
|
|
87
|
+
if os.path.exists(self.properties_file):
|
|
88
|
+
with open(self.properties_file, 'r') as f:
|
|
89
|
+
data = json.load(f)
|
|
90
|
+
|
|
91
|
+
# Convert hex strings back to QColor objects
|
|
92
|
+
for label, props in data.items():
|
|
93
|
+
self.label_properties[label] = {
|
|
94
|
+
'color': QColor(props['color']),
|
|
95
|
+
'radius': props['radius'],
|
|
96
|
+
'opacity': props['opacity']
|
|
97
|
+
}
|
|
98
|
+
return True
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Error loading label properties: {e}")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def add_label_property(self, label, color, radius, opacity):
|
|
104
|
+
"""Add or update a label property"""
|
|
105
|
+
self.label_properties[label] = {
|
|
106
|
+
'color': color,
|
|
107
|
+
'radius': radius,
|
|
108
|
+
'opacity': opacity
|
|
109
|
+
}
|
|
110
|
+
self.save_properties()
|
|
111
|
+
|
|
112
|
+
def get_label_property(self, label):
|
|
113
|
+
"""Get properties for a specific label"""
|
|
114
|
+
return self.label_properties.get(label, None)
|
|
115
|
+
|
|
116
|
+
def get_all_labels(self):
|
|
117
|
+
"""Get all saved label names"""
|
|
118
|
+
return list(self.label_properties.keys())
|
|
119
|
+
|
|
120
|
+
def remove_label_property(self, label):
|
|
121
|
+
"""Remove a label property"""
|
|
122
|
+
if label in self.label_properties:
|
|
123
|
+
del self.label_properties[label]
|
|
124
|
+
self.save_properties()
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
class PaintSettingsDialog(QDialog):
|
|
129
|
+
saved_labels = {}
|
|
130
|
+
label_manager = LabelPropertiesManager()
|
|
131
|
+
def __init__(self, parent=None, current_color=None, current_radius=None, current_opacity=None, current_label=None):
|
|
132
|
+
super().__init__(parent)
|
|
133
|
+
self.setWindowTitle("Paint Settings")
|
|
134
|
+
|
|
135
|
+
# Initialize with current values or defaults
|
|
136
|
+
self.color = current_color if current_color else QColor(255, 0, 0)
|
|
137
|
+
self.radius = current_radius if current_radius else 3
|
|
138
|
+
self.opacity = current_opacity if current_opacity else 255
|
|
139
|
+
self.label = current_label if current_label else ""
|
|
140
|
+
|
|
141
|
+
layout = QFormLayout()
|
|
142
|
+
|
|
143
|
+
# Label selection/input - MODIFIED
|
|
144
|
+
label_layout = QHBoxLayout()
|
|
145
|
+
|
|
146
|
+
self.label_combo = QComboBox()
|
|
147
|
+
self.label_combo.setEditable(True)
|
|
148
|
+
self.label_combo.setPlaceholderText("Enter new label or select existing")
|
|
149
|
+
|
|
150
|
+
# Populate combo box with saved labels
|
|
151
|
+
self.label_combo.addItem("") # Empty option for new labels
|
|
152
|
+
for label in self.label_manager.get_all_labels():
|
|
153
|
+
self.label_combo.addItem(label)
|
|
154
|
+
|
|
155
|
+
# Set current label if provided
|
|
156
|
+
if self.label:
|
|
157
|
+
index = self.label_combo.findText(self.label)
|
|
158
|
+
if index >= 0:
|
|
159
|
+
self.label_combo.setCurrentIndex(index)
|
|
160
|
+
else:
|
|
161
|
+
self.label_combo.setCurrentText(self.label)
|
|
162
|
+
|
|
163
|
+
self.label_combo.currentTextChanged.connect(self.on_label_selected)
|
|
164
|
+
label_layout.addWidget(self.label_combo)
|
|
165
|
+
|
|
166
|
+
layout.addRow("Label:", label_layout)
|
|
167
|
+
|
|
168
|
+
# Color selection
|
|
169
|
+
self.color_button = QPushButton("Choose Color")
|
|
170
|
+
self.color_button.clicked.connect(self.choose_color)
|
|
171
|
+
self.update_color_button() # NEW METHOD CALL
|
|
172
|
+
layout.addRow("Color:", self.color_button)
|
|
173
|
+
|
|
174
|
+
# Radius slider
|
|
175
|
+
self.radius_slider = QSlider(Qt.Orientation.Horizontal)
|
|
176
|
+
self.radius_slider.setRange(1, 100)
|
|
177
|
+
self.radius_slider.setValue(self.radius)
|
|
178
|
+
self.radius_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
179
|
+
self.radius_slider.setTickInterval(10)
|
|
180
|
+
layout.addRow("Radius:", self.radius_slider)
|
|
181
|
+
|
|
182
|
+
self.radius_spinbox = QSpinBox()
|
|
183
|
+
self.radius_spinbox.setRange(1, 100)
|
|
184
|
+
self.radius_spinbox.setValue(self.radius)
|
|
185
|
+
self.radius_spinbox.valueChanged.connect(self.radius_slider.setValue)
|
|
186
|
+
self.radius_slider.valueChanged.connect(self.radius_spinbox.setValue)
|
|
187
|
+
layout.addRow("", self.radius_spinbox)
|
|
188
|
+
|
|
189
|
+
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
190
|
+
self.buttons.accepted.connect(self.accept)
|
|
191
|
+
self.buttons.rejected.connect(self.reject)
|
|
192
|
+
layout.addRow(self.buttons)
|
|
193
|
+
|
|
194
|
+
self.setLayout(layout)
|
|
195
|
+
|
|
196
|
+
def on_label_selected(self, text):
|
|
197
|
+
"""Handle label selection from combo box"""
|
|
198
|
+
if text:
|
|
199
|
+
label_props = self.label_manager.get_label_property(text)
|
|
200
|
+
if label_props:
|
|
201
|
+
# Load saved settings for this label
|
|
202
|
+
self.color = label_props['color']
|
|
203
|
+
self.radius = label_props['radius']
|
|
204
|
+
self.opacity = label_props['opacity']
|
|
205
|
+
|
|
206
|
+
# Update UI elements
|
|
207
|
+
self.radius_slider.setValue(self.radius)
|
|
208
|
+
self.radius_spinbox.setValue(self.radius)
|
|
209
|
+
self.update_color_button()
|
|
210
|
+
|
|
211
|
+
def update_color_button(self):
|
|
212
|
+
"""Update color button appearance to show current color"""
|
|
213
|
+
color_style = f"background-color: rgb({self.color.red()}, {self.color.green()}, {self.color.blue()}); color: {'white' if self.color.lightness() < 128 else 'black'};"
|
|
214
|
+
self.color_button.setStyleSheet(color_style)
|
|
215
|
+
self.color_button.setText(f"Color: {self.color.name()}")
|
|
216
|
+
|
|
217
|
+
def choose_color(self):
|
|
218
|
+
color = QColorDialog.getColor(initial=self.color)
|
|
219
|
+
if color.isValid():
|
|
220
|
+
self.color = color
|
|
221
|
+
self.update_color_button() # NEW METHOD CALL
|
|
222
|
+
|
|
223
|
+
def get_settings(self):
|
|
224
|
+
self.radius = self.radius_slider.value()
|
|
225
|
+
self.label = self.label_combo.currentText().strip()
|
|
226
|
+
|
|
227
|
+
# Save label settings if label is not empty - NEW FUNCTIONALITY
|
|
228
|
+
if self.label:
|
|
229
|
+
self.label_manager.add_label_property(self.label, self.color, self.radius, self.opacity)
|
|
230
|
+
|
|
231
|
+
return self.color, self.radius, self.opacity, self.label
|
|
232
|
+
|
|
233
|
+
def add_overlay(self, overlay_pixmap):
|
|
234
|
+
"""Add an overlay pixmap that stays aligned with the base image"""
|
|
235
|
+
if not self.base_pixmap:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# Remove existing overlay if any
|
|
239
|
+
self.remove_overlay()
|
|
240
|
+
|
|
241
|
+
# Ensure overlay matches base image dimensions
|
|
242
|
+
base_size = self.base_pixmap.size()
|
|
243
|
+
if overlay_pixmap.size() != base_size:
|
|
244
|
+
overlay_pixmap = overlay_pixmap.scaled(
|
|
245
|
+
base_size,
|
|
246
|
+
Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
247
|
+
Qt.TransformationMode.SmoothTransformation
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Create overlay pixmap item
|
|
251
|
+
self.overlay_pixmap_item = self.scene.addPixmap(overlay_pixmap)
|
|
252
|
+
self.overlay_pixmap_item.setZValue(1) # Above base image
|
|
253
|
+
self.overlay_pixmap_item.setPos(0, 0) # Same position as base image
|
|
254
|
+
|
|
255
|
+
# Set opacity (default 50%)
|
|
256
|
+
if not hasattr(self, 'overlay_opacity'):
|
|
257
|
+
self.overlay_opacity = 128
|
|
258
|
+
self.overlay_pixmap_item.setOpacity(self.overlay_opacity / 255.0)
|
|
259
|
+
|
|
260
|
+
# Update scene
|
|
261
|
+
self.scene.update()
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
def set_overlay_opacity(self, opacity):
|
|
265
|
+
"""Set opacity of the overlay layer (0-255)"""
|
|
266
|
+
if self.overlay_pixmap_item:
|
|
267
|
+
self.overlay_opacity = max(0, min(255, opacity))
|
|
268
|
+
self.overlay_pixmap_item.setOpacity(self.overlay_opacity / 255.0)
|
|
269
|
+
self.scene.update()
|
|
270
|
+
return True
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
def toggle_overlay_visibility(self):
|
|
274
|
+
"""Toggle visibility of the overlay"""
|
|
275
|
+
if self.overlay_pixmap_item:
|
|
276
|
+
is_visible = self.overlay_pixmap_item.isVisible()
|
|
277
|
+
self.overlay_pixmap_item.setVisible(not is_visible)
|
|
278
|
+
self.scene.update()
|
|
279
|
+
return not is_visible
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def remove_overlay(self):
|
|
283
|
+
"""Remove overlay if exists"""
|
|
284
|
+
if self.overlay_pixmap_item:
|
|
285
|
+
self.scene.removeItem(self.overlay_pixmap_item)
|
|
286
|
+
self.overlay_pixmap_item = None
|
|
287
|
+
self.scene.update()
|
|
288
|
+
return True
|
|
289
|
+
return False
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from PyQt6.QtWidgets import (
|
|
7
|
+
QGraphicsEllipseItem, QComboBox, QGraphicsRectItem, QInputDialog, QGraphicsItem, QGraphicsItemGroup, QGraphicsPixmapItem, QGraphicsOpacityEffect, QGraphicsView, QGraphicsScene, QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
|
|
8
|
+
QFileDialog, QWidget, QMessageBox, QHBoxLayout, QColorDialog, QDialog, QSlider, QFormLayout, QDialogButtonBox, QGridLayout, QProgressDialog, QCheckBox, QSpinBox, QSplashScreen, QMenu
|
|
9
|
+
)
|
|
10
|
+
from PyQt6.QtGui import QPixmap, QMouseEvent, QImage, QPainter, QColor, QPen, QBrush, QCursor, QIcon, QPainterPath, QFont
|
|
11
|
+
from PyQt6.QtCore import Qt, QPoint, QPointF, QTimer, QThread, pyqtSignal, QSize, QRectF, QObject, QLineF
|
|
12
|
+
import gc
|
|
13
|
+
import math
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
class PointItem(QGraphicsEllipseItem):
|
|
17
|
+
"""Custom graphics item for representing a point/dot with fixed size"""
|
|
18
|
+
def __init__(self, label, x, y, radius, color, opacity=100):
|
|
19
|
+
# Store properties
|
|
20
|
+
self.fixed_label = label
|
|
21
|
+
self.fixed_x = x
|
|
22
|
+
self.fixed_y = y
|
|
23
|
+
self._fixed_radius = radius
|
|
24
|
+
self.fixed_color = QColor(color)
|
|
25
|
+
self.fixed_opacity = int(255 * opacity / 100)
|
|
26
|
+
|
|
27
|
+
# Create the ellipse using the fixed radius
|
|
28
|
+
super().__init__(x - radius, y - radius, radius * 2, radius * 2)
|
|
29
|
+
|
|
30
|
+
self.setPen(QPen(Qt.PenStyle.NoPen))
|
|
31
|
+
self.fixed_color.setAlpha(self.fixed_opacity)
|
|
32
|
+
self.setBrush(QBrush(self.fixed_color))
|
|
33
|
+
self.setZValue(10) # Ensure it's drawn above the image
|
|
34
|
+
|
|
35
|
+
# Disable any modifications
|
|
36
|
+
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
|
37
|
+
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
|
38
|
+
|
|
39
|
+
def get_position(self):
|
|
40
|
+
"""Always return the original center point"""
|
|
41
|
+
return QPointF(self.fixed_x, self.fixed_y)
|
|
42
|
+
|
|
43
|
+
def boundingRect(self):
|
|
44
|
+
"""Always return the original bounding rect with fixed radius"""
|
|
45
|
+
return QRectF(self.fixed_x - self._fixed_radius,
|
|
46
|
+
self.fixed_y - self._fixed_radius,
|
|
47
|
+
self._fixed_radius * 2,
|
|
48
|
+
self._fixed_radius * 2)
|
|
49
|
+
|
|
50
|
+
def shape(self):
|
|
51
|
+
"""Ensure the shape remains fixed"""
|
|
52
|
+
path = QPainterPath()
|
|
53
|
+
path.addEllipse(QRectF(self.fixed_x - self._fixed_radius,
|
|
54
|
+
self.fixed_y - self._fixed_radius,
|
|
55
|
+
self._fixed_radius * 2,
|
|
56
|
+
self._fixed_radius * 2))
|
|
57
|
+
return path
|
|
58
|
+
|
|
59
|
+
def paint(self, painter, option, widget=None):
|
|
60
|
+
"""Recreate the original ellipse with fixed radius and color"""
|
|
61
|
+
painter.setBrush(QBrush(self.fixed_color))
|
|
62
|
+
painter.setPen(QPen(Qt.PenStyle.NoPen))
|
|
63
|
+
painter.drawEllipse(QRectF(self.fixed_x - self._fixed_radius,
|
|
64
|
+
self.fixed_y - self._fixed_radius,
|
|
65
|
+
self._fixed_radius * 2,
|
|
66
|
+
self._fixed_radius * 2))
|