kotones-auto-assistant/tools/hsv_range_tool_qt.py

406 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PyQt6.QtWidgets import (QApplication, QWidget, QMainWindow, QLabel,
QHBoxLayout, QVBoxLayout, QPushButton, QSlider, QFileDialog, QSizePolicy, QFrame,
QLineEdit, QMessageBox)
from PyQt6.QtGui import QImage, QPixmap, QColor, QMouseEvent
from PyQt6.QtCore import Qt
import sys
import cv2
import numpy as np
import tempfile
import os
class HSVRangeTool(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('HSV Range Tool')
self.setMinimumSize(1200, 600)
self.image = None
self.hsv_image = None
self.color_picker_mode = None # 'min' 或 'max' 或 None
self.init_ui()
self.center_window()
def init_ui(self):
# 创建中央部件和主布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# 创建控制面板
control_panel = QWidget()
control_panel.setFixedWidth(300)
control_layout = QVBoxLayout(control_panel)
# 添加载入图片按钮
load_layout = QHBoxLayout()
open_btn = QPushButton('打开图片')
paste_btn = QPushButton('粘贴图片')
open_btn.clicked.connect(self.on_open_image)
paste_btn.clicked.connect(self.on_paste_image)
load_layout.addWidget(open_btn)
load_layout.addWidget(paste_btn)
control_layout.addLayout(load_layout)
# 在控制面板添加取色器按钮
picker_layout = QHBoxLayout()
self.picker_min_btn = QPushButton('取色器(MIN)')
self.picker_max_btn = QPushButton('取色器(MAX)')
self.picker_min_btn.clicked.connect(lambda: self.enable_color_picker('min'))
self.picker_max_btn.clicked.connect(lambda: self.enable_color_picker('max'))
picker_layout.addWidget(self.picker_min_btn)
picker_layout.addWidget(self.picker_max_btn)
control_layout.addLayout(picker_layout)
# 添加模式选择
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("模式:"))
self.remove_color_btn = QPushButton("移除颜色")
self.keep_color_btn = QPushButton("保留颜色")
self.remove_color_btn.setCheckable(True)
self.keep_color_btn.setCheckable(True)
self.remove_color_btn.setChecked(True) # 默认选中移除颜色
self.keep_color_btn.setChecked(False)
# 按钮互斥
self.remove_color_btn.clicked.connect(self.on_mode_change)
self.keep_color_btn.clicked.connect(self.on_mode_change)
mode_layout.addWidget(self.remove_color_btn)
mode_layout.addWidget(self.keep_color_btn)
control_layout.addLayout(mode_layout)
# 添加颜色预览框
preview_layout = QHBoxLayout()
self.min_color_preview = QFrame()
self.max_color_preview = QFrame()
for preview in (self.min_color_preview, self.max_color_preview):
preview.setFixedSize(50, 50)
preview.setFrameShape(QFrame.Shape.Box)
preview.setStyleSheet("background-color: black;")
preview_layout.addWidget(QLabel("Min Color:"))
preview_layout.addWidget(self.min_color_preview)
preview_layout.addWidget(QLabel("Max Color:"))
preview_layout.addWidget(self.max_color_preview)
control_layout.addLayout(preview_layout)
# HSV 滑块标签
hsv_labels = ['H min', 'S min', 'V min', 'H max', 'S max', 'V max']
self.sliders = []
self.value_edits = []
# 创建 HSV 滑块
for i, label in enumerate(hsv_labels):
# 创建水平布局来放置标签、滑块和值编辑框
slider_layout = QHBoxLayout()
# 添加标签
slider_label = QLabel(label)
slider_label.setFixedWidth(50)
slider_layout.addWidget(slider_label)
# 创建滑块
slider = QSlider(Qt.Orientation.Horizontal)
slider.setMinimum(0)
slider.setMaximum(255) # 所有滑块最大值设为255
if i % 3 == 0: # H通道
slider.setMaximum(179)
if i == 3: # H max 默认值设为179
slider.setValue(179)
elif i > 3: # S max和V max默认值设为255
slider.setValue(255)
slider.valueChanged.connect(self.on_slider)
slider_layout.addWidget(slider)
# 添加值编辑框
value_edit = QLineEdit(str(slider.value()))
value_edit.setFixedWidth(40)
value_edit.textChanged.connect(lambda text, s=slider: self.on_edit_change(text, s))
slider_layout.addWidget(value_edit)
self.sliders.append(slider)
self.value_edits.append(value_edit)
control_layout.addLayout(slider_layout)
# 添加复制按钮
copy_btn = QPushButton('复制HSV范围')
copy_btn.clicked.connect(self.copy_hsv_range)
control_layout.addWidget(copy_btn)
# 创建图像显示标签
self.image_label = QLabel()
self.image_label.setStyleSheet("background-color: black;")
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.image_label.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Expanding
)
self.image_label.mousePressEvent = self.on_image_click
# 创建图片容器并设置布局
image_container = QWidget()
image_layout = QVBoxLayout(image_container)
image_layout.addWidget(self.image_label)
# 添加到主布局
main_layout.addWidget(control_panel)
main_layout.addWidget(image_container, stretch=1)
# 设置主布局的边距和间距
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
def center_window(self):
# 将窗口移到屏幕中央
screen = QApplication.primaryScreen()
if screen is None:
return
screen_geometry = screen.geometry()
size = self.geometry()
self.move(
(screen_geometry.width() - size.width()) // 2,
(screen_geometry.height() - size.height()) // 2
)
def on_open_image(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"选择图片",
"",
"图片文件 (*.jpg *.png)"
)
if file_name:
self.image = cv2.imread(file_name)
if self.image is not None:
self.hsv_image = cv2.cvtColor(self.image, cv2.COLOR_BGR2HSV)
self.update_image()
def on_paste_image(self):
# 从剪贴板获取图片
clipboard = QApplication.clipboard()
if clipboard is None:
QMessageBox.warning(self, "错误", "无法访问剪贴板")
return
mime_data = clipboard.mimeData()
if mime_data is None:
QMessageBox.warning(self, "错误", "无法获取剪贴板数据")
return
if mime_data.hasImage():
# 获取剪贴板中的图片
qt_image = clipboard.image()
if not qt_image.isNull():
# 将Qt图像保存为临时文件
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
temp_path = temp_file.name
# 保存Qt图像为PNG文件
if qt_image.save(temp_path, 'PNG'):
# 使用OpenCV读取临时文件
self.image = cv2.imread(temp_path)
if self.image is not None:
self.hsv_image = cv2.cvtColor(self.image, cv2.COLOR_BGR2HSV)
self.update_image()
else:
QMessageBox.warning(self, "错误", "无法读取图片数据")
# 删除临时文件
try:
os.unlink(temp_path)
except:
pass
else:
QMessageBox.warning(self, "错误", "无法保存图片数据")
try:
os.unlink(temp_path)
except:
pass
else:
QMessageBox.warning(self, "错误", "剪贴板中的图片无效")
else:
QMessageBox.information(self, "提示", "剪贴板中没有图片")
def opencv_hsv_to_qt_hsv(self, h, s, v):
# OpenCV 的 H 范围是 0-179需要转换到 Qt 的 0-359
h = int(h * 2)
return h, s, v
def qt_hsv_to_opencv_hsv(self, h, s, v):
# Qt 的 H 范围是 0-359需要转换到 OpenCV 的 0-179
h = int(h / 2)
return h, s, v
def on_slider(self):
# 更新值编辑框
for i, (slider, edit) in enumerate(zip(self.sliders, self.value_edits)):
edit.setText(str(slider.value()))
# 更新颜色预览框
h_min, s_min, v_min = [self.sliders[i].value() for i in range(3)]
h_max, s_max, v_max = [self.sliders[i].value() for i in range(3, 6)]
# 转换 OpenCV HSV 到 Qt HSV
qt_h_min, qt_s_min, qt_v_min = self.opencv_hsv_to_qt_hsv(h_min, s_min, v_min)
qt_h_max, qt_s_max, qt_v_max = self.opencv_hsv_to_qt_hsv(h_max, s_max, v_max)
# 更新最小值颜色预览
min_color = QColor()
min_color.setHsv(qt_h_min, qt_s_min, qt_v_min)
self.min_color_preview.setStyleSheet(f"background-color: {min_color.name()};")
# 更新最大值颜色预览
max_color = QColor()
max_color.setHsv(qt_h_max, qt_s_max, qt_v_max)
self.max_color_preview.setStyleSheet(f"background-color: {max_color.name()};")
if self.image is not None:
self.update_image()
def enable_color_picker(self, mode):
self.color_picker_mode = mode
if mode == 'min':
self.picker_min_btn.setStyleSheet('background-color: yellow')
self.picker_max_btn.setStyleSheet('')
else:
self.picker_min_btn.setStyleSheet('')
self.picker_max_btn.setStyleSheet('background-color: yellow')
def on_image_click(self, ev: QMouseEvent | None) -> None:
if ev is None or not self.color_picker_mode or self.hsv_image is None:
return
# 获取点击位置
pixmap = self.image_label.pixmap()
if pixmap:
# 计算图像的实际显示位置和缩放比例
label_size = self.image_label.size()
pixmap_size = pixmap.size()
# 计算缩放后的图像位置
scale = min(label_size.width() / pixmap_size.width(),
label_size.height() / pixmap_size.height())
scaled_width = int(pixmap_size.width() * scale)
scaled_height = int(pixmap_size.height() * scale)
# 计算图像在Label中的偏移量
x_offset = (label_size.width() - scaled_width) // 2
y_offset = (label_size.height() - scaled_height) // 2
# 将点击位置转换为图像坐标
x = int((ev.position().x() - x_offset) / scale)
y = int((ev.position().y() - y_offset) / scale)
if 0 <= x < self.hsv_image.shape[1] and 0 <= y < self.hsv_image.shape[0]:
# 获取HSV值
hsv = self.hsv_image[y, x]
# 更新对应的滑块
if self.color_picker_mode == 'min':
self.sliders[0].setValue(int(hsv[0]))
self.sliders[1].setValue(int(hsv[1]))
self.sliders[2].setValue(int(hsv[2]))
else:
self.sliders[3].setValue(int(hsv[0]))
self.sliders[4].setValue(int(hsv[1]))
self.sliders[5].setValue(int(hsv[2]))
# 关闭取色器模式
self.color_picker_mode = None
self.picker_min_btn.setStyleSheet('')
self.picker_max_btn.setStyleSheet('')
def update_image(self):
if self.image is None or self.hsv_image is None:
return
# 获取 HSV 范围
h_min = self.sliders[0].value()
s_min = self.sliders[1].value()
v_min = self.sliders[2].value()
h_max = self.sliders[3].value()
s_max = self.sliders[4].value()
v_max = self.sliders[5].value()
# 创建掩码
lower = np.array([h_min, s_min, v_min])
upper = np.array([h_max, s_max, v_max])
mask = cv2.inRange(self.hsv_image, lower, upper)
# 根据模式选择是保留还是移除颜色
if self.keep_color_btn.isChecked():
result = cv2.bitwise_and(self.image, self.image, mask=mask)
else: # 移除颜色模式
result = cv2.bitwise_and(self.image, self.image, mask=cv2.bitwise_not(mask))
# 转换为 QImage 并显示
height, width = result.shape[:2]
bytes_per_line = 3 * width
qt_image = QImage(
result.data,
width,
height,
bytes_per_line,
QImage.Format.Format_BGR888
)
# 创建适应label大小的pixmap
pixmap = QPixmap.fromImage(qt_image)
scaled_pixmap = pixmap.scaled(
self.image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
self.image_label.setPixmap(scaled_pixmap)
def resizeEvent(self, event):
super().resizeEvent(event)
if hasattr(self, 'image_label') and self.image is not None:
self.update_image()
def on_edit_change(self, text, slider):
try:
value = int(text)
max_value = slider.maximum()
if 0 <= value <= max_value:
slider.setValue(value)
except ValueError:
pass
def copy_hsv_range(self):
h_min = self.sliders[0].value()
s_min = self.sliders[1].value()
v_min = self.sliders[2].value()
h_max = self.sliders[3].value()
s_max = self.sliders[4].value()
v_max = self.sliders[5].value()
hsv_range = f"(({h_min}, {s_min}, {v_min}), ({h_max}, {s_max}, {v_max}))"
clip = QApplication.clipboard()
if clip:
clip.setText(hsv_range)
QMessageBox.information(self, "提示", "HSV范围已复制到剪贴板")
else:
QMessageBox.warning(self, "提示", "无法访问剪贴板")
def on_mode_change(self):
# 设置按钮互斥
sender = self.sender()
if sender == self.remove_color_btn:
self.keep_color_btn.setChecked(not self.remove_color_btn.isChecked())
else:
self.remove_color_btn.setChecked(not self.keep_color_btn.isChecked())
# 更新图片
self.update_image()
def main():
app = QApplication(sys.argv)
window = HSVRangeTool()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()