#!/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)