mirror of
https://github.com/HChaZZY/Any2MIF.git
synced 2025-12-06 10:33:49 +08:00
432 lines
16 KiB
Python
432 lines
16 KiB
Python
#!/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) |