Files
Any2MIF/ui/image_toolbar.py
2025-03-07 15:32:58 +08:00

432 lines
16 KiB
Python
Raw 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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2025 Any2MIF Project
# All rights reserved.
"""
Any2MIF - 图像处理工具栏
负责图像的灰度化、二值化等操作
"""
import os
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSlider, QGroupBox, QToolButton,
QSpinBox, QComboBox, QCheckBox, QFileDialog
)
from PyQt6.QtCore import Qt, QSettings, pyqtSignal, QTimer
from PyQt6.QtGui import QPixmap, QImage
from PIL import Image, ImageQt
from core.image_processor import ImageProcessor
class ImageToolbar(QWidget):
"""图像处理工具栏"""
def __init__(self, image_processor: ImageProcessor, settings: QSettings, preview_label=None, parent=None):
"""初始化图像处理工具栏"""
super().__init__(parent)
self.image_processor = image_processor
self.settings = settings
self.preview_label = preview_label # 外部传入的预览标签
self.original_image = None
self.processed_image = None
self._last_size = None # 初始化上一次窗口大小
self._fixed_preview_height = None # 固定的预览高度
self._init_ui()
self._load_settings()
self._connect_signals()
# 默认禁用工具栏
self.setEnabled(False)
def _init_ui(self):
"""初始化UI"""
# 创建主布局
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# 创建标题标签
title_label = QLabel("图像处理")
title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(title_label)
# 创建2x2网格布局来放置四个功能区
grid_layout = QHBoxLayout()
layout.addLayout(grid_layout)
# 创建左侧和右侧的垂直布局
left_column = QVBoxLayout()
right_column = QVBoxLayout()
grid_layout.addLayout(left_column)
grid_layout.addLayout(right_column)
# 创建灰度化组 (左上)
grayscale_group = QGroupBox("灰度化")
grayscale_layout = QVBoxLayout(grayscale_group)
left_column.addWidget(grayscale_group)
# 灰度化方法选择
self.grayscale_method_combo = QComboBox()
self.grayscale_method_combo.addItems(["平均值法", "加权平均法", "最大值法"])
grayscale_layout.addWidget(self.grayscale_method_combo)
# 灰度化按钮
self.grayscale_button = QPushButton("应用灰度化")
grayscale_layout.addWidget(self.grayscale_button)
# 创建二值化组 (右上)
threshold_group = QGroupBox("二值化")
threshold_layout = QVBoxLayout(threshold_group)
right_column.addWidget(threshold_group)
# 阈值滑块
threshold_layout_h = QHBoxLayout()
threshold_layout.addLayout(threshold_layout_h)
threshold_layout_h.addWidget(QLabel("阈值:"))
self.threshold_slider = QSlider(Qt.Orientation.Horizontal)
self.threshold_slider.setRange(0, 255)
self.threshold_slider.setValue(128)
threshold_layout_h.addWidget(self.threshold_slider)
self.threshold_value_label = QLabel("128")
threshold_layout_h.addWidget(self.threshold_value_label)
# 二值化方法选择
self.threshold_method_combo = QComboBox()
self.threshold_method_combo.addItems(["固定阈值", "自适应阈值", "Otsu阈值"])
threshold_layout.addWidget(self.threshold_method_combo)
# 二值化按钮
self.threshold_button = QPushButton("应用二值化")
threshold_layout.addWidget(self.threshold_button)
# 创建降噪组 (左下)
denoise_group = QGroupBox("降噪处理")
denoise_layout = QVBoxLayout(denoise_group)
left_column.addWidget(denoise_group)
# 降噪方法选择
self.denoise_method_combo = QComboBox()
self.denoise_method_combo.addItems(["中值滤波", "高斯滤波", "双边滤波"])
denoise_layout.addWidget(self.denoise_method_combo)
# 降噪强度
denoise_strength_layout = QHBoxLayout()
denoise_layout.addLayout(denoise_strength_layout)
denoise_strength_layout.addWidget(QLabel("强度:"))
self.denoise_strength_slider = QSlider(Qt.Orientation.Horizontal)
self.denoise_strength_slider.setRange(1, 10)
self.denoise_strength_slider.setValue(3)
denoise_strength_layout.addWidget(self.denoise_strength_slider)
self.denoise_strength_label = QLabel("3")
denoise_strength_layout.addWidget(self.denoise_strength_label)
# 降噪按钮
self.denoise_button = QPushButton("应用降噪")
denoise_layout.addWidget(self.denoise_button)
# 创建尺寸标准化组 (右下)
resize_group = QGroupBox("尺寸标准化")
resize_layout = QVBoxLayout(resize_group)
right_column.addWidget(resize_group)
# 尺寸设置
size_layout = QHBoxLayout()
resize_layout.addLayout(size_layout)
size_layout.addWidget(QLabel("宽度:"))
self.width_spin = QSpinBox()
self.width_spin.setRange(1, 1000)
self.width_spin.setValue(128)
size_layout.addWidget(self.width_spin)
size_layout.addWidget(QLabel("高度:"))
self.height_spin = QSpinBox()
self.height_spin.setRange(1, 1000)
self.height_spin.setValue(128)
size_layout.addWidget(self.height_spin)
# 保持纵横比
self.keep_aspect_check = QCheckBox("保持纵横比")
self.keep_aspect_check.setChecked(True)
resize_layout.addWidget(self.keep_aspect_check)
# 调整尺寸按钮
self.resize_button = QPushButton("应用尺寸调整")
resize_layout.addWidget(self.resize_button)
# 创建操作按钮布局
button_layout = QHBoxLayout()
layout.addLayout(button_layout)
# 重置按钮
self.reset_button = QPushButton("重置图像")
button_layout.addWidget(self.reset_button)
# 保存按钮
self.save_button = QPushButton("保存图像")
button_layout.addWidget(self.save_button)
def _load_settings(self):
"""加载设置"""
# 加载灰度化方法
grayscale_method_index = self.settings.value("image/grayscale_method_index", 0, int)
self.grayscale_method_combo.setCurrentIndex(grayscale_method_index)
# 加载二值化设置
threshold = self.settings.value("image/threshold", 128, int)
self.threshold_slider.setValue(threshold)
self.threshold_value_label.setText(str(threshold))
threshold_method_index = self.settings.value("image/threshold_method_index", 0, int)
self.threshold_method_combo.setCurrentIndex(threshold_method_index)
# 加载降噪设置
denoise_method_index = self.settings.value("image/denoise_method_index", 0, int)
self.denoise_method_combo.setCurrentIndex(denoise_method_index)
denoise_strength = self.settings.value("image/denoise_strength", 3, int)
self.denoise_strength_slider.setValue(denoise_strength)
self.denoise_strength_label.setText(str(denoise_strength))
# 加载尺寸设置
width = self.settings.value("image/width", 128, int)
self.width_spin.setValue(width)
height = self.settings.value("image/height", 128, int)
self.height_spin.setValue(height)
keep_aspect = self.settings.value("image/keep_aspect", True, bool)
self.keep_aspect_check.setChecked(keep_aspect)
def _connect_signals(self):
"""连接信号和槽"""
# 灰度化
self.grayscale_button.clicked.connect(self._apply_grayscale)
self.grayscale_method_combo.currentIndexChanged.connect(self._save_settings)
# 二值化
self.threshold_slider.valueChanged.connect(self._on_threshold_changed)
self.threshold_button.clicked.connect(self._apply_threshold)
self.threshold_method_combo.currentIndexChanged.connect(self._save_settings)
# 降噪
self.denoise_strength_slider.valueChanged.connect(self._on_denoise_strength_changed)
self.denoise_button.clicked.connect(self._apply_denoise)
self.denoise_method_combo.currentIndexChanged.connect(self._save_settings)
# 尺寸调整
self.resize_button.clicked.connect(self._apply_resize)
self.width_spin.valueChanged.connect(self._save_settings)
self.height_spin.valueChanged.connect(self._save_settings)
self.keep_aspect_check.toggled.connect(self._save_settings)
# 操作按钮
self.reset_button.clicked.connect(self._reset_image)
self.save_button.clicked.connect(self._save_image)
def _on_threshold_changed(self, value):
"""处理阈值变化事件"""
self.threshold_value_label.setText(str(value))
self._save_settings()
def _on_denoise_strength_changed(self, value):
"""处理降噪强度变化事件"""
self.denoise_strength_label.setText(str(value))
self._save_settings()
def _save_settings(self):
"""保存设置"""
# 保存灰度化设置
self.settings.setValue("image/grayscale_method_index", self.grayscale_method_combo.currentIndex())
# 保存二值化设置
self.settings.setValue("image/threshold", self.threshold_slider.value())
self.settings.setValue("image/threshold_method_index", self.threshold_method_combo.currentIndex())
# 保存降噪设置
self.settings.setValue("image/denoise_method_index", self.denoise_method_combo.currentIndex())
self.settings.setValue("image/denoise_strength", self.denoise_strength_slider.value())
# 保存尺寸设置
self.settings.setValue("image/width", self.width_spin.value())
self.settings.setValue("image/height", self.height_spin.value())
self.settings.setValue("image/keep_aspect", self.keep_aspect_check.isChecked())
def load_image(self, image_path):
"""加载图像"""
if not os.path.exists(image_path):
return
# 加载原始图像
self.original_image = Image.open(image_path)
self.processed_image = self.original_image.copy()
# 如果是首次加载图像,设置固定的预览高度
if self._fixed_preview_height is None and self.preview_label is not None:
self._fixed_preview_height = self.preview_label.height()
# 设置预览标签的固定高度
self.preview_label.setFixedHeight(self._fixed_preview_height)
# 更新预览
self._update_preview()
def _update_preview(self):
"""更新预览"""
if self.processed_image is None or self.preview_label is None:
return
# 调整图像大小以适应预览区域
preview_width = self.preview_label.width()
# 使用固定的预览高度,如果没有设置则使用当前高度
preview_height = self._fixed_preview_height if self._fixed_preview_height is not None else self.preview_label.height()
# 确保预览区域有有效尺寸
if preview_width <= 10 or preview_height <= 10:
return
# 计算缩放比例 - 使用宽度比例,但保持图片纵横比
width_ratio = preview_width / self.processed_image.width
height_ratio = preview_height / self.processed_image.height
# 使用较小的比例以确保图片完全适应预览区域,同时保持纵横比
ratio = min(width_ratio, height_ratio)
# 计算新尺寸 - 保持纵横比
new_width = max(int(self.processed_image.width * ratio), 1)
new_height = max(int(self.processed_image.height * ratio), 1)
# 调整图像大小
preview_image = self.processed_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
# 转换为QPixmap并显示
q_image = ImageQt.ImageQt(preview_image)
pixmap = QPixmap.fromImage(q_image)
self.preview_label.setPixmap(pixmap)
# 确保预览标签的大小策略正确
self.preview_label.setScaledContents(False) # 不自动缩放内容
def _apply_grayscale(self):
"""应用灰度化"""
if self.processed_image is None:
return
# 获取灰度化方法
method_index = self.grayscale_method_combo.currentIndex()
method_map = {0: "average", 1: "weighted", 2: "max"}
method = method_map[method_index]
# 应用灰度化
self.processed_image = self.image_processor.grayscale(self.processed_image, method)
# 更新预览
self._update_preview()
def _apply_threshold(self):
"""应用二值化"""
if self.processed_image is None:
return
# 获取二值化参数
threshold = self.threshold_slider.value()
method_index = self.threshold_method_combo.currentIndex()
method_map = {0: "fixed", 1: "adaptive", 2: "otsu"}
method = method_map[method_index]
# 应用二值化
self.processed_image = self.image_processor.threshold(self.processed_image, threshold, method)
# 更新预览
self._update_preview()
def _apply_denoise(self):
"""应用降噪"""
if self.processed_image is None:
return
# 获取降噪参数
method_index = self.denoise_method_combo.currentIndex()
method_map = {0: "median", 1: "gaussian", 2: "bilateral"}
method = method_map[method_index]
strength = self.denoise_strength_slider.value()
# 应用降噪
self.processed_image = self.image_processor.denoise(self.processed_image, method, strength)
# 更新预览
self._update_preview()
def _apply_resize(self):
"""应用尺寸调整"""
if self.processed_image is None:
return
# 获取尺寸参数
width = self.width_spin.value()
height = self.height_spin.value()
keep_aspect = self.keep_aspect_check.isChecked()
# 应用尺寸调整
self.processed_image = self.image_processor.resize(self.processed_image, width, height, keep_aspect)
# 更新预览
self._update_preview()
def _reset_image(self):
"""重置图像"""
if self.original_image is None:
return
# 重置为原始图像
self.processed_image = self.original_image.copy()
# 更新预览
self._update_preview()
def _save_image(self):
"""保存图像"""
if self.processed_image is None:
return
# 获取保存路径
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存图像",
os.path.expanduser("~"),
"图像文件 (*.png *.jpg *.bmp)"
)
if file_path:
# 保存图像
self.processed_image.save(file_path)
def get_processed_image(self):
"""获取处理后的图像"""
return self.processed_image
def resizeEvent(self, event):
"""处理大小调整事件"""
# 获取当前窗口大小
current_size = event.size()
# 调用父类的resizeEvent
super().resizeEvent(event)
# 如果已经设置了固定预览高度,确保预览标签保持该高度
if self.preview_label is not None and self._fixed_preview_height is not None:
self.preview_label.setFixedHeight(self._fixed_preview_height)
# 更新上一次窗口大小
self._last_size = current_size
# 使用QTimer延迟更新预览确保布局已完全调整
QTimer.singleShot(0, self._update_preview)