import colorsys
import typing
import PySide2
from PySide2.QtCore import Qt, QTimer, Signal
from PySide2.QtGui import QColor, QPainter, QPixmap, QValidator
from PySide2.QtWidgets import QWidget, QColorDialog, QDialogButtonBox, QDialog
from colorsys import rgb_to_hsv, hsv_to_rgb
from arthropod_describer.common.label_hierarchy import Node
from arthropod_describer.common.state import State
from arthropod_describer.ui_color_tolerance_dialog import Ui_ColorToleranceDialog
[docs]class ColorToleranceDialog(QDialog):
def __init__(self, state: State, parent: typing.Optional[PySide2.QtWidgets.QWidget] = None,
f: PySide2.QtCore.Qt.WindowFlags = Qt.WindowFlags()):
super().__init__(parent, f)
self.state: State = state
self.ui = Ui_ColorToleranceDialog()
self.ui.setupUi(self)
self._lblColor_pixmap = QPixmap(self.ui.lblColor.minimumSize())
self._lblVTolerancePreview_pixmap = QPixmap(self.ui.lblVTolerancePreview.minimumSize())
self._lblHSTolerancePreview_pixmap = QPixmap(self.ui.lblHSTolerancePreview.minimumSize())
self._selected_color: QColor = QColor(255, 255, 255)
self.ui.btnSetColor.clicked.connect(self._pick_color)
self._parent_node: typing.Optional[Node] = None
self.ui.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self.accept)
self.ui.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(self.reject)
self.ui.spinBoxHueTolerance.valueChanged.connect(self._update_tolerance_preview)
self.ui.spinBoxSaturationTolerance.valueChanged.connect(self._update_tolerance_preview)
self.ui.spinBoxValueTolerance.valueChanged.connect(self._update_tolerance_preview)
self.ui.sliderPreviewV.valueChanged.connect(self._update_tolerance_preview)
self._float_v = 0
self._animation_speed = 0.02
self._animation_direction = 1
self._animation_timer = QTimer()
self._animation_timer.setInterval(50)
self._animation_timer.timeout.connect(self._update_animation)
self.setWindowModality(Qt.ApplicationModal)
def _update_tolerance_preview(self):
# Display the range of colors that falls within the selected tolerances.
# 2D projection with a Value slider (whose range also reflects the selected range),
selected_hue = self._selected_color.hue()
selected_saturation = self._selected_color.saturation()
selected_value = self._selected_color.value()
self.ui.lblSelectedColor.setText(f"HSV == ({selected_hue}, {selected_saturation}, {selected_value})")
min_hue = selected_hue - self.ui.spinBoxHueTolerance.value() + 360 # Add 360 to ensure we never use negative values, but `min_hue < max_hue` always holds. Later, using `% 360` may be required.
max_hue = selected_hue + self.ui.spinBoxHueTolerance.value() + 360 # Add 360 to ensure we never use negative values, but `min_hue < max_hue` always holds. Later, using `% 360` may be required.
min_saturation = max(0, selected_saturation - self.ui.spinBoxSaturationTolerance.value())
max_saturation = min(255, selected_saturation + self.ui.spinBoxSaturationTolerance.value())
min_value = max(0, selected_value - self.ui.spinBoxValueTolerance.value())
max_value = min(255, selected_value + self.ui.spinBoxValueTolerance.value())
self.ui.lblSelectedRange.setText(f"HSV range == ({min_hue}..{max_hue}, {min_saturation}..{max_saturation}, {min_value}..{max_value})")
self.ui.sliderPreviewV.setRange(min_value, max_value)
if not self.ui.checkBoxAnimate.isChecked():
self._float_v = self.ui.sliderPreviewV.value()
self._lblColor_pixmap.fill(self._selected_color)
self.ui.lblColor.setPixmap(self._lblColor_pixmap)
# 1D gradient V
painter = QPainter(self._lblVTolerancePreview_pixmap)
for x in range(self._lblVTolerancePreview_pixmap.width()):
current_color = QColor()
current_color.setHsv(255, 0, min_value + (max_value - min_value) * x / self._lblVTolerancePreview_pixmap.width())
painter.setPen(current_color)
painter.drawLine(x, 0, x, self._lblVTolerancePreview_pixmap.height())
self.ui.lblVTolerancePreview.setPixmap(self._lblVTolerancePreview_pixmap)
# 2D gradient HS
painter = QPainter(self._lblHSTolerancePreview_pixmap)
for x in range(self._lblHSTolerancePreview_pixmap.width()):
for y in range(self._lblHSTolerancePreview_pixmap.height()):
current_color = QColor()
current_color.setHsv(min_hue + (max_hue - min_hue) * x / self._lblHSTolerancePreview_pixmap.width(),
max_saturation - (max_saturation - min_saturation) * y / self._lblHSTolerancePreview_pixmap.height(),
self.ui.sliderPreviewV.value()
)
painter.setPen(current_color)
painter.drawPoint(x, y)
self.ui.lblHSTolerancePreview.setPixmap(self._lblHSTolerancePreview_pixmap)
def _update_animation(self):
if not self.ui.checkBoxAnimate.isChecked() or self.ui.sliderPreviewV.minimum() >= self.ui.sliderPreviewV.maximum():
return
# Move the V slider, change the direction when at the beginning/end.
self._float_v = self._float_v + (self.ui.sliderPreviewV.maximum() - self.ui.sliderPreviewV.minimum()) * self._animation_speed * self._animation_direction
if self._float_v < self.ui.sliderPreviewV.minimum() or self._float_v > self.ui.sliderPreviewV.maximum():
self._animation_direction = -self._animation_direction
self._float_v = min(self.ui.sliderPreviewV.maximum(), max(self.ui.sliderPreviewV.minimum(), self._float_v))
self.ui.sliderPreviewV.setValue(round(self._float_v))
self._update_tolerance_preview()
def _pick_color(self):
color = QColorDialog.getColor(initial=self._selected_color)
if color.isValid():
self._selected_color = color
self._update_tolerance_preview()
[docs] def accept(self):
super().accept()
self._animation_timer.stop()
[docs] def reject(self):
super().reject()
self._animation_timer.stop()
[docs] def get_color_and_tolerances(self):
self._update_tolerance_preview()
self._animation_timer.start()
if self.exec_() == QDialog.Accepted:
# Return the selected color and tolerances
print(f"returning {self._selected_color}, {self.ui.spinBoxHueTolerance.value()}, {self.ui.spinBoxSaturationTolerance.value()}, {self.ui.spinBoxValueTolerance.value()}")
return self._selected_color, self.ui.spinBoxHueTolerance.value(), self.ui.spinBoxSaturationTolerance.value(), self.ui.spinBoxValueTolerance.value()
else:
# Return no color and tolerances
print(f"returning {None}, {0}, {0}, {0}")
return None, 0, 0, 0