14、SpinBox与Horizontal Scroll Bar

博客提到设定两个控件的maximum为100,然后转到槽,还给出了转载来源https://www.cnblogs.com/xixixing/p/10929558.html 。

设定这两个控件maximum为100,转到槽

void MainWindow::on_horizontalSlider_valueChanged(int value)
{
    ui->spinBox->setValue(value);
}

void MainWindow::on_spinBox_valueChanged(int arg1)
{
    ui->horizontalSlider->setValue(arg1);
}

 

转载于:https://www.cnblogs.com/xixixing/p/10929558.html

import sys import cv2 import numpy as np from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QDialog, QLabel, QPushButton, QFileDialog, QTextEdit, QTabWidget, QMessageBox, QProgressBar, QSlider, QComboBox, QGroupBox, QGridLayout, QToolBar, QStatusBar, QDockWidget, QSplitter, QScrollArea, QMenu, QSpinBox, QDoubleSpinBox, QCheckBox, QRadioButton, QButtonGroup # 添加缺失的导入 ) from PySide6.QtGui import ( QAction, QPixmap, QImage, QPainter, QPen, QColor, QIcon, QKeySequence, QTransform, QCursor ) from PySide6.QtCore import Qt, QThread, Signal, QPoint, QSize, QRect import matplotlib matplotlib.use('Agg') # 使用Agg后端,不显示图形窗口 import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import os import math from scipy import ndimage try: from skimage.feature import graycomatrix, graycoprops except ImportError: from skimage.feature import greycomatrix as graycomatrix, greycoprops as graycoprops plt.rcParams["font.family"] = ["SimHei"] # 仅保留 SimHei(黑体) plt.rcParams["axes.unicode_minus"] = False class ImageProcessingThread(QThread): """图像处理的工作线程,避免界面卡顿""" finished = Signal(object) def __init__(self, function, *args): super().__init__() self.function = function self.args = args def run(self): result = self.function(*self.args) self.finished.emit(result) class ImageViewer(QWidget): """图像显示组件,支持缩放和平移""" def __init__(self, parent=None): super().__init__(parent) self.image = QImage() self.scale_factor = 1.0 self.dragging = False self.last_pos = QPoint() self.setMouseTracking(True) layout = QVBoxLayout(self) self.label = QLabel(self) self.label.setAlignment(Qt.AlignCenter) self.label.setMinimumSize(1, 1) # 允许缩小 layout.addWidget(self.label) def set_image(self, image): self.image = image self.update_pixmap() def update_pixmap(self): if not self.image.isNull(): scaled_pixmap = QPixmap.fromImage(self.image).scaled( self.image.width() * self.scale_factor, self.image.height() * self.scale_factor, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.label.setPixmap(scaled_pixmap) def wheelEvent(self, event): """鼠标滚轮缩放""" delta = event.angleDelta().y() if delta > 0: self.scale_factor *= 1.1 else: self.scale_factor *= 0.9 self.update_pixmap() def mousePressEvent(self, event): """鼠标按下开始拖动""" if event.button() == Qt.LeftButton: self.dragging = True self.last_pos = event.position().toPoint() # 修改后 def mouseMoveEvent(self, event): """鼠标拖动图像""" if self.dragging: delta = event.pos() - self.last_pos scroll_bar = self.parent().horizontalScrollBar() scroll_bar.setValue(scroll_bar.value() - delta.x()) scroll_bar = self.parent().verticalScrollBar() scroll_bar.setValue(scroll_bar.value() - delta.y()) self.last_pos = event.pos() def mouseReleaseEvent(self, event): """鼠标释放结束拖动""" if event.button() == Qt.LeftButton: self.dragging = False def resizeEvent(self, event): """窗口大小变化时更新图像显示""" self.update_pixmap() super().resizeEvent(event) class HistogramWidget(QWidget): """直方图显示组件""" def __init__(self, parent=None): super().__init__(parent) self.figure = Figure(figsize=(5, 3), dpi=100) self.canvas = FigureCanvas(self.figure) layout = QVBoxLayout(self) layout.addWidget(self.canvas) self.axes = self.figure.add_subplot(111) def update_histogram(self, image): """更新直方图显示""" self.axes.clear() if image.ndim == 3: # 彩色图像 colors = ('b', 'g', 'r') for i, color in enumerate(colors): hist = cv2.calcHist([image], [i], None, [256], [0, 256]) self.axes.plot(hist, color=color) else: # 灰度图像 hist = cv2.calcHist([image], [0], None, [256], [0, 256]) self.axes.plot(hist, color='black') self.axes.set_xlim([0, 256]) self.axes.set_title('图像直方图') self.axes.set_xlabel('像素值') self.axes.set_ylabel('像素数量') self.figure.tight_layout() self.canvas.draw() class GLCMWidget(QWidget): """灰度共生矩阵(GLCM)特征显示组件""" def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) self.text_edit = QTextEdit() self.text_edit.setReadOnly(True) layout.addWidget(self.text_edit) def update_glcm(self, image): """更新GLCM特征显示""" if image.ndim == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 量化到16级灰度以减少计算量 image_quantized = (image // 16).astype(np.uint8) # 计算GLCM矩阵 (距离=1, 角度=0, 45, 90, 135度) glcm = graycomatrix(image_quantized, distances=[1], angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels=16, symmetric=True, normed=True) # 计算各种特征 contrast = graycoprops(glcm, 'contrast')[0] dissimilarity = graycoprops(glcm, 'dissimilarity')[0] homogeneity = graycoprops(glcm, 'homogeneity')[0] energy = graycoprops(glcm, 'energy')[0] correlation = graycoprops(glcm, 'correlation')[0] asm = graycoprops(glcm, 'ASM')[0] # 显示结果 result = "灰度共生矩阵(GLCM)特征:\n\n" result += f"对比度: {contrast}\n" result += f"相异性: {dissimilarity}\n" result += f"同质性: {homogeneity}\n" result += f"能量: {energy}\n" result += f"相关性: {correlation}\n" result += f"角二阶矩: {asm}\n" self.text_edit.setText(result) class ProcessingDialog(QDialog): def __init__(self, title, parent=None): super().__init__(parent) self.setWindowTitle(title) self.layout = QGridLayout(self) self.setLayout(self.layout) self.row = 0 def add_slider(self, label_text, min_val, max_val, default_val, step=1, callback=None): """添加滑块控件""" label = QLabel(label_text) slider = QSlider(Qt.Horizontal) slider.setMinimum(min_val) slider.setMaximum(max_val) slider.setValue(default_val) slider.setSingleStep(step) spinbox = QSpinBox() # 修正拼写错误 spinbox.setMinimum(min_val) spinbox.setMaximum(max_val) spinbox.setValue(default_val) spinbox.setSingleStep(step) # 同步滑块和数值框 slider.valueChanged.connect(spinbox.setValue) spinbox.valueChanged.connect(slider.setValue) if callback: slider.valueChanged.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(slider, self.row, 1) self.layout.addWidget(spinbox, self.row, 2) self.row += 1 return slider, spinbox def add_double_slider(self, label_text, min_val, max_val, default_val, step=0.1, decimals=1, callback=None): """添加浮点数滑块控件""" label = QLabel(label_text) slider = QSlider(Qt.Horizontal) slider.setMinimum(int(min_val * 10)) slider.setMaximum(int(max_val * 10)) slider.setValue(int(default_val * 10)) spinbox = QDoubleSpinBox() spinbox.setMinimum(min_val) spinbox.setMaximum(max_val) spinbox.setValue(default_val) spinbox.setSingleStep(step) spinbox.setDecimals(decimals) # 同步滑块和数值框 def update_slider(value): slider.setValue(int(value * 10)) def update_spinbox(value): spinbox.setValue(value / 10) slider.valueChanged.connect(update_spinbox) spinbox.valueChanged.connect(update_slider) if callback: slider.valueChanged.connect(lambda: callback(spinbox.value())) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(slider, self.row, 1) self.layout.addWidget(spinbox, self.row, 2) self.row += 1 return slider, spinbox def add_combo_box(self, label_text, items, default_index=0, callback=None): """添加下拉选择框""" label = QLabel(label_text) combo_box = QComboBox() combo_box.addItems(items) combo_box.setCurrentIndex(default_index) if callback: combo_box.currentIndexChanged.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addWidget(combo_box, self.row, 1, 1, 2) self.row += 1 return combo_box def add_checkbox(self, label_text, default_state=False, callback=None): """添加复选框""" checkbox = QCheckBox(label_text) checkbox.setChecked(default_state) if callback: checkbox.stateChanged.connect(callback) self.layout.addWidget(checkbox, self.row, 0, 1, 3) self.row += 1 return checkbox def add_radio_buttons(self, label_text, options, default_index=0, callback=None): """添加单选按钮组""" label = QLabel(label_text) button_group = QButtonGroup(self) layout = QHBoxLayout() for i, option in enumerate(options): radio = QRadioButton(option) if i == default_index: radio.setChecked(True) button_group.addButton(radio, i) layout.addWidget(radio) if callback: button_group.buttonClicked.connect(callback) self.layout.addWidget(label, self.row, 0) self.layout.addLayout(layout, self.row, 1, 1, 2) self.row += 1 return button_group def add_button_box(self): """添加确认和取消按钮""" button_layout = QHBoxLayout() ok_button = QPushButton("确定") cancel_button = QPushButton("取消") ok_button.clicked.connect(self.accept) cancel_button.clicked.connect(self.reject) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) self.layout.addLayout(button_layout, self.row, 0, 1, 3) self.row += 1 return ok_button, cancel_button class MainWindow(QMainWindow): """主窗口类""" def __init__(self): super().__init__() self.setWindowTitle("数字图像处理系统") self.setGeometry(100, 100, 1200, 800) # 初始化变量 self.original_image = None # 原始图像 self.processed_image = None # 处理后的图像 self.current_image = None # 当前显示的图像 self.history = [] # 操作历史 self.history_index = -1 # 当前历史位置 # 创建中心部件 self.central_widget = QWidget() self.setCentralWidget(self.central_widget) # 创建主布局 self.main_layout = QHBoxLayout(self.central_widget) # 创建左侧面板 self.left_panel = QVBoxLayout() # 创建工具栏 self.create_toolbar() # 创建图像显示区域 self.create_image_viewer() # 创建右侧面板 self.right_panel = QVBoxLayout() # 创建处理历史标签页 self.create_history_tabs() # 添加分割器 self.splitter = QSplitter(Qt.Horizontal) left_widget = QWidget() left_widget.setLayout(self.left_panel) right_widget = QWidget() right_widget.setLayout(self.right_panel) self.splitter.addWidget(left_widget) self.splitter.addWidget(right_widget) self.splitter.setSizes([800, 400]) # 初始大小 self.main_layout.addWidget(self.splitter) # 创建菜单 self.create_menu() # 状态栏 self.statusBar().showMessage("就绪") def create_menu(self): """创建菜单栏""" # 文件菜单 file_menu = self.menuBar().addMenu("文件") open_action = QAction("打开", self) open_action.setShortcut(QKeySequence.Open) open_action.triggered.connect(self.open_image) file_menu.addAction(open_action) save_action = QAction("保存", self) save_action.setShortcut(QKeySequence.Save) save_action.triggered.connect(self.save_image) file_menu.addAction(save_action) save_as_action = QAction("另存为", self) save_as_action.setShortcut(QKeySequence.SaveAs) save_as_action.setShortcut("Ctrl+Shift+S") save_as_action.triggered.connect(self.save_image_as) file_menu.addAction(save_as_action) file_menu.addSeparator() exit_action = QAction("退出", self) exit_action.setShortcut(QKeySequence.Quit) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 编辑菜单 edit_menu = self.menuBar().addMenu("编辑") undo_action = QAction("撤销", self) undo_action.setShortcut(QKeySequence.Undo) undo_action.triggered.connect(self.undo) edit_menu.addAction(undo_action) redo_action = QAction("重做", self) redo_action.setShortcut(QKeySequence.Redo) redo_action.triggered.connect(self.redo) edit_menu.addAction(redo_action) # 处理菜单 process_menu = self.menuBar().addMenu("图像处理") # 图像转换子菜单 convert_menu = QMenu("图像转换", self) rgb_to_gray_action = QAction("RGB转灰度", self) rgb_to_gray_action.triggered.connect(self.rgb_to_gray) convert_menu.addAction(rgb_to_gray_action) resize_action = QAction("调整分辨率", self) resize_action.triggered.connect(self.resize_image) convert_menu.addAction(resize_action) process_menu.addMenu(convert_menu) # 图像增强子菜单 enhance_menu = QMenu("图像增强", self) histogram_equalization_action = QAction("直方图均衡化", self) histogram_equalization_action.triggered.connect(self.histogram_equalization) enhance_menu.addAction(histogram_equalization_action) log_transform_action = QAction("对数变换", self) log_transform_action.triggered.connect(self.log_transform) enhance_menu.addAction(log_transform_action) power_law_action = QAction("幂律变换", self) power_law_action.triggered.connect(self.power_law_transform) enhance_menu.addAction(power_law_action) blur_menu = QMenu("平滑滤波", self) mean_blur_action = QAction("均值滤波", self) mean_blur_action.triggered.connect(lambda: self.spatial_filtering("均值滤波")) blur_menu.addAction(mean_blur_action) gaussian_blur_action = QAction("高斯滤波", self) gaussian_blur_action.triggered.connect(lambda: self.spatial_filtering("高斯滤波")) blur_menu.addAction(gaussian_blur_action) median_blur_action = QAction("中值滤波", self) median_blur_action.triggered.connect(lambda: self.spatial_filtering("中值滤波")) blur_menu.addAction(median_blur_action) enhance_menu.addMenu(blur_menu) sharpen_menu = QMenu("锐化滤波", self) sobel_action = QAction("Sobel算子", self) sobel_action.triggered.connect(lambda: self.spatial_filtering("Sobel算子")) sharpen_menu.addAction(sobel_action) prewitt_action = QAction("Prewitt算子", self) prewitt_action.triggered.connect(lambda: self.spatial_filtering("Prewitt算子")) sharpen_menu.addAction(prewitt_action) laplacian_action = QAction("Laplacian算子", self) laplacian_action.triggered.connect(lambda: self.spatial_filtering("Laplacian算子")) sharpen_menu.addAction(laplacian_action) enhance_menu.addMenu(sharpen_menu) process_menu.addMenu(enhance_menu) # 图像复原子菜单 restore_menu = QMenu("图像复原", self) motion_deblur_action = QAction("运动模糊复原", self) motion_deblur_action.triggered.connect(self.motion_deblur) restore_menu.addAction(motion_deblur_action) gaussian_noise_removal_action = QAction("高斯噪声去除", self) gaussian_noise_removal_action.triggered.connect(lambda: self.noise_removal("高斯噪声")) restore_menu.addAction(gaussian_noise_removal_action) salt_pepper_noise_removal_action = QAction("椒盐噪声去除", self) salt_pepper_noise_removal_action.triggered.connect(lambda: self.noise_removal("椒盐噪声")) restore_menu.addAction(salt_pepper_noise_removal_action) process_menu.addMenu(restore_menu) # 几何变换子菜单 geometric_menu = QMenu("几何变换", self) translate_action = QAction("平移", self) translate_action.triggered.connect(self.translate_image) geometric_menu.addAction(translate_action) rotate_action = QAction("旋转", self) rotate_action.triggered.connect(self.rotate_image) geometric_menu.addAction(rotate_action) scale_action = QAction("缩放", self) scale_action.triggered.connect(self.scale_image) geometric_menu.addAction(scale_action) flip_action = QAction("镜像", self) flip_action.triggered.connect(self.flip_image) geometric_menu.addAction(flip_action) process_menu.addMenu(geometric_menu) # 形态学处理子菜单 morphology_menu = QMenu("形态学处理", self) erosion_action = QAction("腐蚀", self) erosion_action.triggered.connect(lambda: self.morphological_operation("腐蚀")) morphology_menu.addAction(erosion_action) dilation_action = QAction("膨胀", self) dilation_action.triggered.connect(lambda: self.morphological_operation("膨胀")) morphology_menu.addAction(dilation_action) opening_action = QAction("开运算", self) opening_action.triggered.connect(lambda: self.morphological_operation("开运算")) morphology_menu.addAction(opening_action) closing_action = QAction("闭运算", self) closing_action.triggered.connect(lambda: self.morphological_operation("闭运算")) morphology_menu.addAction(closing_action) edge_extraction_action = QAction("边界提取", self) edge_extraction_action.triggered.connect(self.edge_extraction) morphology_menu.addAction(edge_extraction_action) process_menu.addMenu(morphology_menu) # 图像分割子菜单 segmentation_menu = QMenu("图像分割", self) threshold_action = QAction("阈值分割", self) threshold_action.triggered.connect(self.threshold_segmentation) segmentation_menu.addAction(threshold_action) adaptive_threshold_action = QAction("自适应阈值分割", self) adaptive_threshold_action.triggered.connect(self.adaptive_threshold_segmentation) segmentation_menu.addAction(adaptive_threshold_action) watershed_action = QAction("分水岭分割", self) watershed_action.triggered.connect(self.watershed_segmentation) segmentation_menu.addAction(watershed_action) process_menu.addMenu(segmentation_menu) # 图像描述子菜单 description_menu = QMenu("图像描述", self) hu_moments_action = QAction("计算Hu不变矩", self) hu_moments_action.triggered.connect(self.calculate_hu_moments) description_menu.addAction(hu_moments_action) glcm_action = QAction("计算灰度共生矩阵", self) glcm_action.triggered.connect(self.calculate_glcm) description_menu.addAction(glcm_action) process_menu.addMenu(description_menu) # 视图菜单 view_menu = self.menuBar().addMenu("视图") zoom_in_action = QAction("放大", self) zoom_in_action.setShortcut("Ctrl++") zoom_in_action.triggered.connect(self.zoom_in) view_menu.addAction(zoom_in_action) zoom_out_action = QAction("缩小", self) zoom_out_action.setShortcut("Ctrl+-") zoom_out_action.triggered.connect(self.zoom_out) view_menu.addAction(zoom_out_action) fit_to_window_action = QAction("适应窗口", self) fit_to_window_action.setShortcut("Ctrl+F") fit_to_window_action.triggered.connect(self.fit_to_window) view_menu.addAction(fit_to_window_action) # 帮助菜单 help_menu = self.menuBar().addMenu("帮助") about_action = QAction("关于", self) about_action.triggered.connect(self.about) help_menu.addAction(about_action) help_action = QAction("帮助", self) help_action.triggered.connect(self.show_help) help_menu.addAction(help_action) def create_toolbar(self): """创建工具栏""" toolbar = QToolBar("工具栏") self.addToolBar(toolbar) # 文件操作 open_action = QAction(QIcon.fromTheme("document-open"), "打开", self) open_action.triggered.connect(self.open_image) toolbar.addAction(open_action) save_action = QAction(QIcon.fromTheme("document-save"), "保存", self) save_action.triggered.connect(self.save_image) toolbar.addAction(save_action) toolbar.addSeparator() # 编辑操作 undo_action = QAction(QIcon.fromTheme("edit-undo"), "撤销", self) undo_action.triggered.connect(self.undo) toolbar.addAction(undo_action) redo_action = QAction(QIcon.fromTheme("edit-redo"), "重做", self) redo_action.triggered.connect(self.redo) toolbar.addAction(redo_action) toolbar.addSeparator() # 视图操作 zoom_in_action = QAction(QIcon.fromTheme("zoom-in"), "放大", self) zoom_in_action.triggered.connect(self.zoom_in) toolbar.addAction(zoom_in_action) zoom_out_action = QAction(QIcon.fromTheme("zoom-out"), "缩小", self) zoom_out_action.triggered.connect(self.zoom_out) toolbar.addAction(zoom_out_action) fit_to_window_action = QAction(QIcon.fromTheme("zoom-fit-best"), "适应窗口", self) fit_to_window_action.triggered.connect(self.fit_to_window) toolbar.addAction(fit_to_window_action) toolbar.addSeparator() # 图像比较 compare_action = QAction(QIcon.fromTheme("view-compare"), "比较原图", self) compare_action.triggered.connect(self.compare_with_original) toolbar.addAction(compare_action) def create_image_viewer(self): """创建图像显示区域""" # 创建滚动区域 self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) # 创建图像查看器 self.image_viewer = ImageViewer() self.scroll_area.setWidget(self.image_viewer) # 添加到左侧面板 self.left_panel.addWidget(self.scroll_area) # 创建直方图显示区域 self.histogram_widget = HistogramWidget() self.left_panel.addWidget(self.histogram_widget) def create_history_tabs(self): """创建历史标签页""" self.history_tabs = QTabWidget() # 原始图像标签页 self.original_tab = QWidget() self.original_tab_layout = QVBoxLayout(self.original_tab) self.original_viewer = ImageViewer() self.original_tab_layout.addWidget(self.original_viewer) self.history_tabs.addTab(self.original_tab, "原始图像") # 处理后图像标签页 self.processed_tab = QWidget() self.processed_tab_layout = QVBoxLayout(self.processed_tab) self.processed_viewer = ImageViewer() self.processed_tab_layout.addWidget(self.processed_viewer) self.history_tabs.addTab(self.processed_tab, "处理后图像") # 描述信息标签页 self.info_tab = QWidget() self.info_tab_layout = QVBoxLayout(self.info_tab) self.info_text = QTextEdit() self.info_text.setReadOnly(True) self.info_tab_layout.addWidget(self.info_text) self.history_tabs.addTab(self.info_tab, "图像信息") # GLCM特征标签页 self.glcm_tab = QWidget() self.glcm_tab_layout = QVBoxLayout(self.glcm_tab) self.glcm_widget = GLCMWidget() self.glcm_tab_layout.addWidget(self.glcm_widget) self.history_tabs.addTab(self.glcm_tab, "GLCM特征") # 添加到右侧面板 self.right_panel.addWidget(self.history_tabs) def open_image(self): """打开图像文件""" file_path, _ = QFileDialog.getOpenFileName( self, "打开图像", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.gif *.tiff)" ) if file_path: self.statusBar().showMessage(f"正在加载图像: {file_path}") # 在单独的线程中加载图像,避免界面卡顿 thread = ImageProcessingThread(self._load_image, file_path) thread.finished.connect(self._on_image_loaded) thread.start() def _load_image(self, file_path): """在线程中加载图像""" image = cv2.imread(file_path) if image is None: return None, file_path return image, file_path def _on_image_loaded(self, result): """图像加载完成后的回调函数""" image, file_path = result if image is None: QMessageBox.critical(self, "错误", f"无法加载图像: {file_path}") self.statusBar().showMessage("加载图像失败") return self.original_image = image self.processed_image = image.copy() self.current_image = image.copy() # 显示图像 self.display_image(self.current_image) # 更新原始图像查看器 self.original_viewer.set_image(self.cv_to_qimage(self.original_image)) # 更新图像信息 self.update_image_info() # 清空历史 self.history = [self.original_image.copy()] self.history_index = 0 self.statusBar().showMessage(f"已加载图像: {os.path.basename(file_path)}") def save_image(self): """保存当前图像""" if self.processed_image is None: QMessageBox.warning(self, "警告", "没有可保存的图像") return if not hasattr(self, 'current_file_path'): self.save_image_as() else: try: cv2.imwrite(self.current_file_path, self.processed_image) self.statusBar().showMessage(f"已保存图像: {os.path.basename(self.current_file_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"保存图像失败: {str(e)}") self.statusBar().showMessage("保存图像失败") def save_image_as(self): """另存为图像""" if self.processed_image is None: QMessageBox.warning(self, "警告", "没有可保存的图像") return file_path, _ = QFileDialog.getSaveFileName( self, "保存图像", "", "PNG (*.png);;JPEG (*.jpg);;BMP (*.bmp);;TIFF (*.tiff)" ) if file_path: try: # 确保保存的是RGB格式 if len(self.processed_image.shape) == 3: image_to_save = cv2.cvtColor(self.processed_image, cv2.COLOR_BGR2RGB) else: image_to_save = self.processed_image cv2.imwrite(file_path, image_to_save) self.current_file_path = file_path self.statusBar().showMessage(f"已保存图像: {os.path.basename(file_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"保存图像失败: {str(e)}") self.statusBar().showMessage("保存图像失败") def display_image(self, image): """显示图像""" if image is None: return qimage = self.cv_to_qimage(image) self.image_viewer.set_image(qimage) # 更新直方图 self.histogram_widget.update_histogram(image) # 更新处理后图像查看器 self.processed_viewer.set_image(qimage) def cv_to_qimage(self, cv_image): """将OpenCV图像转换为Qt图像""" if len(cv_image.shape) == 3: # 彩色图像 height, width, channel = cv_image.shape bytes_per_line = 3 * width qimage = QImage(cv_image.data, width, height, bytes_per_line, QImage.Format_BGR888) else: # 灰度图像 height, width = cv_image.shape bytes_per_line = width qimage = QImage(cv_image.data, width, height, bytes_per_line, QImage.Format_Grayscale8) return qimage def update_image_info(self): """更新图像信息""" if self.original_image is None: return info = "图像信息:\n\n" info += f"尺寸: {self.original_image.shape[1]} x {self.original_image.shape[0]} 像素\n" if len(self.original_image.shape) == 3: info += f"通道数: {self.original_image.shape[2]}\n" info += "类型: 彩色图像\n" else: info += "通道数: 1\n" info += "类型: 灰度图像\n" info += f"数据类型: {self.original_image.dtype}" self.info_text.setText(info) def add_to_history(self, processed_image, operation_name): """添加操作到历史记录""" # 如果当前不在历史的末尾,删除后面的所有历史 if self.history_index < len(self.history) - 1: self.history = self.history[:self.history_index + 1] # 添加新的历史记录 self.history.append(processed_image.copy()) self.history_index += 1 # 更新处理后图像标签页 self.processed_viewer.set_image(self.cv_to_qimage(processed_image)) # 更新历史标签页标题 self.history_tabs.setTabText(1, f"处理后图像 ({operation_name})") def undo(self): """撤销操作""" if self.history_index > 0: self.history_index -= 1 self.processed_image = self.history[self.history_index].copy() self.display_image(self.processed_image) self.statusBar().showMessage("已撤销操作") def redo(self): """重做操作""" if self.history_index < len(self.history) - 1: self.history_index += 1 self.processed_image = self.history[self.history_index].copy() self.display_image(self.processed_image) self.statusBar().showMessage("已重做操作") def zoom_in(self): """放大图像""" self.image_viewer.scale_factor *= 1.1 self.image_viewer.update_pixmap() def zoom_out(self): """缩小图像""" self.image_viewer.scale_factor *= 0.9 self.image_viewer.update_pixmap() def fit_to_window(self): """适应窗口显示""" if self.current_image is None: return # 计算适应窗口的缩放因子 scroll_area_width = self.scroll_area.width() - 20 # 减去边框 scroll_area_height = self.scroll_area.height() - 20 image_width = self.current_image.shape[1] image_height = self.current_image.shape[0] scale_x = scroll_area_width / image_width scale_y = scroll_area_height / image_height self.image_viewer.scale_factor = min(scale_x, scale_y) self.image_viewer.update_pixmap() def compare_with_original(self): """比较处理后的图像原始图像""" if self.original_image is None or self.processed_image is None: return # 创建一个新窗口进行比较 compare_window = QMainWindow() compare_window.setWindowTitle("图像比较") compare_window.resize(1000, 500) # 创建分割器 splitter = QSplitter(Qt.Horizontal) # 左侧显示原始图像 left_widget = QWidget() left_layout = QVBoxLayout(left_widget) left_label = QLabel("原始图像") left_label.setAlignment(Qt.AlignCenter) left_viewer = ImageViewer() left_viewer.set_image(self.cv_to_qimage(self.original_image)) left_layout.addWidget(left_label) left_layout.addWidget(left_viewer) # 右侧显示处理后的图像 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_label = QLabel("处理后图像") right_label.setAlignment(Qt.AlignCenter) right_viewer = ImageViewer() right_viewer.set_image(self.cv_to_qimage(self.processed_image)) right_layout.addWidget(right_label) right_layout.addWidget(right_viewer) # 添加到分割器 splitter.addWidget(left_widget) splitter.addWidget(right_widget) splitter.setSizes([500, 500]) compare_window.setCentralWidget(splitter) compare_window.show() def about(self): """显示关于对话框""" QMessageBox.about(self, "关于数字图像处理系统", "数字图像处理系统\n\n" "基于OpenCV和PySide6开发\n" "支持图像转换、增强、复原、几何变换、形态学处理、分割和描述等功能\n\n" "版本: 1.0.0" ) def show_help(self): """显示帮助对话框""" help_text = ( "数字图像处理系统帮助文档\n\n" "1. 文件操作:\n" " - 打开: 从文件系统加载图像\n" " - 保存: 保存当前处理的图像\n" " - 另存为: 以新文件名保存图像\n\n" "2. 编辑操作:\n" " - 撤销: 撤销上一步操作\n" " - 重做: 恢复撤销的操作\n\n" "3. 图像处理:\n" " - 图像转换: 支持模式转换和分辨率调整\n" " - 图像增强: 包括直方图均衡化、滤波等\n" " - 图像复原: 处理运动模糊和噪声\n" " - 几何变换: 平移、旋转、缩放和镜像\n" " - 形态学处理: 腐蚀、膨胀、开/闭运算等\n" " - 图像分割: 阈值分割和区域分割\n" " - 图像描述: 计算不变矩和灰度共生矩阵\n\n" "4. 视图操作:\n" " - 放大/缩小: 调整图像显示大小\n" " - 适应窗口: 自动调整图像大小以适应窗口\n\n" "5. 比较功能:\n" " - 比较原图: 同时显示原始图像和处理后图像进行对比" ) QMessageBox.information(self, "帮助", help_text) # 图像处理功能实现 def rgb_to_gray(self): """RGB转灰度""" if self.processed_image is None: return if len(self.processed_image.shape) == 2: QMessageBox.warning(self, "警告", "当前图像已经是灰度图像") return thread = ImageProcessingThread(cv2.cvtColor, self.processed_image, cv2.COLOR_BGR2GRAY) thread.finished.connect(lambda result: self._process_finished(result, "RGB转灰度")) thread.start() def resize_image(self): """调整图像分辨率""" if self.processed_image is None: return dialog = ProcessingDialog("调整分辨率") # 获取当前图像尺寸 current_width = self.processed_image.shape[1] current_height = self.processed_image.shape[0] # 添加宽度和高度输入框 width_spinbox = QSpinBox() width_spinbox.setRange(1, 10000) width_spinbox.setValue(current_width) height_spinbox = QSpinBox() height_spinbox.setRange(1, 10000) height_spinbox.setValue(current_height) # 保持比例复选框 keep_ratio_checkbox = QCheckBox("保持比例") keep_ratio_checkbox.setChecked(True) # 添加到对话框 dialog.layout.addWidget(QLabel("宽度:"), 0, 0) dialog.layout.addWidget(width_spinbox, 0, 1) dialog.layout.addWidget(QLabel("像素"), 0, 2) dialog.layout.addWidget(QLabel("高度:"), 1, 0) dialog.layout.addWidget(height_spinbox, 1, 1) dialog.layout.addWidget(QLabel("像素"), 1, 2) dialog.layout.addWidget(keep_ratio_checkbox, 2, 0, 1, 3) # 添加按钮 ok_button, cancel_button = dialog.add_button_box() # 保持比例功能 ratio = current_width / current_height def update_height(): if keep_ratio_checkbox.isChecked(): height_spinbox.setValue(int(width_spinbox.value() / ratio)) def update_width(): if keep_ratio_checkbox.isChecked(): width_spinbox.setValue(int(height_spinbox.value() * ratio)) width_spinbox.valueChanged.connect(update_height) height_spinbox.valueChanged.connect(update_width) if dialog.exec_(): new_width = width_spinbox.value() new_height = height_spinbox.value() # 添加插值方法选择 interpolation_dialog = ProcessingDialog("选择插值方法") methods = ["最近邻", "双线性", "双三次", "Lanczos"] method_combo = interpolation_dialog.add_combo_box("插值方法", methods) if interpolation_dialog.exec_(): method_index = method_combo.currentIndex() interpolation_methods = [ cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4 ] interpolation = interpolation_methods[method_index] thread = ImageProcessingThread(cv2.resize, self.processed_image, (new_width, new_height), interpolation=interpolation) thread.finished.connect(lambda result: self._process_finished(result, "调整分辨率")) thread.start() def histogram_equalization(self): """直方图均衡化""" if self.processed_image is None: return if len(self.processed_image.shape) == 3: # 彩色图像需要先转换到YUV空间 def equalize_color(image): yuv = cv2.cvtColor(image, cv2.COLOR_BGR2YUV) yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0]) return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR) thread = ImageProcessingThread(equalize_color, self.processed_image) else: # 灰度图像直接均衡化 thread = ImageProcessingThread(cv2.equalizeHist, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "直方图均衡化")) thread.start() def log_transform(self): """对数变换""" if self.processed_image is None: return dialog = ProcessingDialog("对数变换") c_slider, c_spinbox = dialog.add_slider("常数C", 1, 100, 25) if dialog.exec_(): c = c_spinbox.value() def log_transform(image): # 确保图像是浮点类型 image_float = image.astype(np.float32) / 255.0 # 应用对数变换 result = c * np.log(1 + image_float) # 归一化到[0, 1] result = cv2.normalize(result, None, 0, 1, cv2.NORM_MINMAX) # 转回uint8 return (result * 255).astype(np.uint8) thread = ImageProcessingThread(log_transform, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "对数变换")) thread.start() def power_law_transform(self): """幂律变换""" if self.processed_image is None: return dialog = ProcessingDialog("幂律变换") gamma_slider, gamma_spinbox = dialog.add_double_slider("伽马值", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): gamma = gamma_spinbox.value() def power_law_transform(image): # 确保图像是浮点类型 image_float = image.astype(np.float32) / 255.0 # 应用幂律变换 result = np.power(image_float, gamma) # 归一化到[0, 1] result = cv2.normalize(result, None, 0, 1, cv2.NORM_MINMAX) # 转回uint8 return (result * 255).astype(np.uint8) thread = ImageProcessingThread(power_law_transform, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"幂律变换 (γ={gamma})")) thread.start() def spatial_filtering(self, filter_type): """空域滤波""" if self.processed_image is None: return dialog = ProcessingDialog(f"{filter_type}参数设置") if filter_type in ["均值滤波", "高斯滤波"]: kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 elif filter_type == "中值滤波": kernel_size, _ = dialog.add_slider("孔径大小", 1, 21, 3, 2) # 奇数 elif filter_type in ["Sobel算子", "Prewitt算子"]: direction_combo = dialog.add_combo_box("方向", ["水平", "垂直", "两者"]) elif filter_type == "Laplacian算子": ksize_combo = dialog.add_combo_box("核大小", ["1", "3", "5", "7"], 1) if dialog.exec_(): if filter_type == "均值滤波": ksize = kernel_size.value() def mean_filter(image): return cv2.blur(image, (ksize, ksize)) thread = ImageProcessingThread(mean_filter, self.processed_image) elif filter_type == "高斯滤波": ksize = kernel_size.value() def gaussian_filter(image): return cv2.GaussianBlur(image, (ksize, ksize), 0) thread = ImageProcessingThread(gaussian_filter, self.processed_image) elif filter_type == "中值滤波": ksize = kernel_size.value() def median_filter(image): return cv2.medianBlur(image, ksize) thread = ImageProcessingThread(median_filter, self.processed_image) elif filter_type == "Sobel算子": direction = direction_combo.currentIndex() def sobel_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if direction == 0: # 水平 sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3) return cv2.convertScaleAbs(sobelx) elif direction == 1: # 垂直 sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3) return cv2.convertScaleAbs(sobely) else: # 两者 sobelx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3) sobely = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3) return cv2.addWeighted(cv2.convertScaleAbs(sobelx), 0.5, cv2.convertScaleAbs(sobely), 0.5, 0) thread = ImageProcessingThread(sobel_filter, self.processed_image) elif filter_type == "Prewitt算子": direction = direction_combo.currentIndex() def prewitt_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) kernelx = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=np.float32) kernely = np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]], dtype=np.float32) if direction == 0: # 水平 prewittx = cv2.filter2D(image, -1, kernelx) return prewittx elif direction == 1: # 垂直 prewitty = cv2.filter2D(image, -1, kernely) return prewitty else: # 两者 prewittx = cv2.filter2D(image, -1, kernelx) prewitty = cv2.filter2D(image, -1, kernely) return cv2.addWeighted(prewittx, 0.5, prewitty, 0.5, 0) thread = ImageProcessingThread(prewitt_filter, self.processed_image) elif filter_type == "Laplacian算子": ksize = [1, 3, 5, 7][ksize_combo.currentIndex()] def laplacian_filter(image): if len(image.shape) == 3: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) return cv2.Laplacian(image, cv2.CV_64F, ksize=ksize) thread = ImageProcessingThread(laplacian_filter, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, filter_type)) thread.start() def motion_deblur(self): """运动模糊复原""" if self.processed_image is None: return dialog = ProcessingDialog("运动模糊复原参数设置") length_slider, length_spinbox = dialog.add_slider("运动长度", 1, 100, 15) angle_slider, angle_spinbox = dialog.add_slider("运动角度", 0, 360, 0) gamma_slider, gamma_spinbox = dialog.add_double_slider("噪声功率谱比", 0.01, 10.0, 0.1, 0.01, 2) if dialog.exec_(): length = length_spinbox.value() angle = angle_spinbox.value() gamma = gamma_spinbox.value() def deblur(image): # 确保图像是灰度图 if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 创建运动模糊核 kernel = np.zeros((length, length), dtype=np.float32) kernel[int((length - 1) / 2), :] = np.ones(length, dtype=np.float32) # 旋转核 M = cv2.getRotationMatrix2D((length / 2, length / 2), angle, 1.0) kernel = cv2.warpAffine(kernel, M, (length, length)) # 归一化 kernel /= length # 傅里叶变换 fft = np.fft.fft2(gray) fft_kernel = np.fft.fft2(kernel, s=gray.shape) # 维纳滤波 H_conj = np.conj(fft_kernel) H_squared = np.abs(fft_kernel) ** 2 G = (H_conj / (H_squared + gamma)) * fft # 逆傅里叶变换 deblurred = np.fft.ifft2(G) deblurred = np.abs(deblurred) # 归一化到0-255 deblurred = cv2.normalize(deblurred, None, 0, 255, cv2.NORM_MINMAX) deblurred = deblurred.ast(np.uint8) return deblurred thread = ImageProcessingThread(deblur, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "运动模糊复原")) thread.start() def noise_removal(self, noise_type): """噪声去除""" if self.processed_image is None: return dialog = ProcessingDialog(f"{noise_type}去除") if noise_type == "高斯噪声": kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 elif noise_type == "椒盐噪声": kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3, 2) # 奇数 if dialog.exec_(): ksize = kernel_size.value() if noise_type == "高斯噪声": def remove_gaussian_noise(image): return cv2.GaussianBlur(image, (ksize, ksize), 0) thread = ImageProcessingThread(remove_gaussian_noise, self.processed_image) elif noise_type == "椒盐噪声": def remove_salt_pepper_noise(image): return cv2.medianBlur(image, ksize) thread = ImageProcessingThread(remove_salt_pepper_noise, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"{noise_type}去除")) thread.start() def translate_image(self): """平移图像""" if self.processed_image is None: return dialog = ProcessingDialog("平移图像") tx_slider, tx_spinbox = dialog.add_slider("水平偏移", -500, 500, 0) ty_slider, ty_spinbox = dialog.add_slider("垂直偏移", -500, 500, 0) if dialog.exec_(): tx = tx_spinbox.value() ty = ty_spinbox.value() def translate(image): M = np.float32([[1, 0, tx], [0, 1, ty]]) return cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) thread = ImageProcessingThread(translate, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"平移 (tx={tx}, ty={ty})")) thread.start() def rotate_image(self): """旋转图像""" if self.processed_image is None: return dialog = ProcessingDialog("旋转图像") angle_slider, angle_spinbox = dialog.add_slider("旋转角度", -180, 180, 0) scale_slider, scale_spinbox = dialog.add_double_slider("缩放比例", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): angle = angle_spinbox.value() scale = scale_spinbox.value() def rotate(image): center = (image.shape[1] // 2, image.shape[0] // 2) M = cv2.getRotationMatrix2D(center, angle, scale) return cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) thread = ImageProcessingThread(rotate, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"旋转 ({angle}°, 缩放{scale}x)")) thread.start() def scale_image(self): """缩放图像""" if self.processed_image is None: return dialog = ProcessingDialog("缩放图像") scale_slider, scale_spinbox = dialog.add_double_slider("缩放比例", 0.1, 5.0, 1.0, 0.1, 1) if dialog.exec_(): scale = scale_spinbox.value() def scale_image(image): new_width = int(image.shape[1] * scale) new_height = int(image.shape[0] * scale) return cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR) thread = ImageProcessingThread(scale_image, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"缩放 ({scale}x)")) thread.start() def flip_image(self): """镜像图像""" if self.processed_image is None: return dialog = ProcessingDialog("镜像图像") flip_type = dialog.add_combo_box("镜像类型", ["水平", "垂直", "水平和垂直"]) if dialog.exec_(): flip_code = flip_type.currentIndex() def flip(image): return cv2.flip(image, flip_code) flip_types = ["水平", "垂直", "水平和垂直"] thread = ImageProcessingThread(flip, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, f"镜像 ({flip_types[flip_code]})")) thread.start() def morphological_operation(self, operation_type): """形态学操作""" if self.processed_image is None: return dialog = ProcessingDialog(f"{operation_type}参数设置") kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3) kernel_shape = dialog.add_combo_box("核形状", ["矩形", "椭圆", "十字形"]) if dialog.exec_(): ksize = kernel_size.value() shape_index = kernel_shape.currentIndex() kernel_shapes = [ cv2.MORPH_RECT, cv2.MORPH_ELLIPSE, cv2.MORPH_CROSS ] kernel = cv2.getStructuringElement(kernel_shapes[shape_index], (ksize, ksize)) if operation_type == "腐蚀": def erode(image): return cv2.erode(image, kernel) thread = ImageProcessingThread(erode, self.processed_image) elif operation_type == "膨胀": def dilate(image): return cv2.dilate(image, kernel) thread = ImageProcessingThread(dilate, self.processed_image) elif operation_type == "开运算": def opening(image): return cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel) thread = ImageProcessingThread(opening, self.processed_image) elif operation_type == "闭运算": def closing(image): return cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel) thread = ImageProcessingThread(closing, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, operation_type)) thread.start() def edge_extraction(self): """边界提取""" if self.processed_image is None: return dialog = ProcessingDialog("边界提取参数设置") kernel_size, _ = dialog.add_slider("核大小", 1, 21, 3) kernel_shape = dialog.add_combo_box("核形状", ["矩形", "椭圆", "十字形"]) if dialog.exec_(): ksize = kernel_size.value() shape_index = kernel_shape.currentIndex() kernel_shapes = [ cv2.MORPH_RECT, cv2.MORPH_ELLIPSE, cv2.MORPH_CROSS ] kernel = cv2.getStructuringElement(kernel_shapes[shape_index], (ksize, ksize)) def extract_edge(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 膨胀 dilated = cv2.dilate(gray, kernel) # 边界提取 return dilated - gray thread = ImageProcessingThread(extract_edge, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "边界提取")) thread.start() def threshold_segmentation(self): """阈值分割""" if self.processed_image is None: return dialog = ProcessingDialog("阈值分割参数设置") threshold_slider, threshold_spinbox = dialog.add_slider("阈值", 0, 255, 127) max_value_slider, max_value_spinbox = dialog.add_slider("最大值", 0, 255, 255) threshold_type = dialog.add_combo_box("阈值类型", [ "二进制阈值", "反二进制阈值", "截断阈值", "零阈值", "反零阈值", "Otsu算法" ]) if dialog.exec_(): threshold = threshold_spinbox.value() max_value = max_value_spinbox.value() type_index = threshold_type.currentIndex() threshold_types = [ cv2.THRESH_BINARY, cv2.THRESH_BINARY_INV, cv2.THRESH_TRUNC, cv2.THRESH_TOZERO, cv2.THRESH_TOZERO_INV, cv2.THRESH_BINARY + cv2.THRESH_OTSU ] def threshold_segment(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() if type_index == 5: # Otsu算法,忽略手动设置的阈值 _, thresh = cv2.threshold(gray, 0, max_value, threshold_types[type_index]) else: _, thresh = cv2.threshold(gray, threshold, max_value, threshold_types[type_index]) return thresh type_names = ["二进制阈值", "反二进制阈值", "截断阈值", "零阈值", "反零阈值", "Otsu算法"] thread = ImageProcessingThread(threshold_segment, self.processed_image) thread.finished.connect( lambda result: self._process_finished(result, f"阈值分割 ({type_names[type_index]})")) thread.start() def adaptive_threshold_segmentation(self): """自适应阈值分割""" if self.processed_image is None: return dialog = ProcessingDialog("自适应阈值分割参数设置") max_value_slider, max_value_spinbox = dialog.add_slider("最大值", 0, 255, 255) method_combo = dialog.add_combo_box("自适应方法", ["均值", "高斯"]) type_combo = dialog.add_combo_box("阈值类型", ["二进制阈值", "反二进制阈值"]) block_size_slider, block_size_spinbox = dialog.add_slider("块大小", 3, 101, 11, 2) # 奇数 c_slider, c_spinbox = dialog.add_double_slider("常数C", -10, 10, 2, 0.1, 1) if dialog.exec_(): max_value = max_value_spinbox.value() method_index = method_combo.currentIndex() type_index = type_combo.currentIndex() block_size = block_size_spinbox.value() c = c_spinbox.value() adaptive_methods = [ cv2.ADAPTIVE_THRESH_MEAN_C, cv2.ADAPTIVE_THRESH_GAUSSIAN_C ] threshold_types = [ cv2.THRESH_BINARY, cv2.THRESH_BINARY_INV ] def adaptive_threshold(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() return cv2.adaptiveThreshold( gray, max_value, adaptive_methods[method_index], threshold_types[type_index], block_size, c ) method_names = ["均值", "高斯"] type_names = ["二进制阈值", "反二进制阈值"] thread = ImageProcessingThread(adaptive_threshold, self.processed_image) thread.finished.connect(lambda result: self._process_finished( result, f"自适应阈值分割 ({method_names[method_index]}, {type_names[type_index]})" )) thread.start() def watershed_segmentation(self): """分水岭分割""" if self.processed_image is None: return if len(self.processed_image.shape) != 3: QMessageBox.warning(self, "警告", "分水岭分割需要彩色图像") return dialog = ProcessingDialog("分水岭分割参数设置") threshold_slider, threshold_spinbox = dialog.add_slider("阈值", 0, 255, 100) morph_size_slider, morph_size_spinbox = dialog.add_slider("形态学操作核大小", 1, 21, 3) if dialog.exec_(): threshold = threshold_spinbox.value() morph_size = morph_size_spinbox.value() def watershed(image): # 转换为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 阈值处理 ret, thresh = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # 噪声去除 kernel = np.ones((morph_size, morph_size), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) # 确定背景区域 sure_bg = cv2.dilate(opening, kernel, iterations=3) # 确定前景区域 dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5) ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # 找到未知区域 sure_fg = np.uint8(sure_fg) unknown = cv2.subtract(sure_bg, sure_fg) # 标记标签 ret, markers = cv2.connectedComponents(sure_fg) # 为所有标签加1,确保背景不是0而是1 markers = markers + 1 # 将未知区域标记为0 markers[unknown == 255] = 0 # 应用分水岭算法 markers = cv2.watershed(image, markers) image[markers == -1] = [0, 0, 255] # 标记边界为红色 return image thread = ImageProcessingThread(watershed, self.processed_image) thread.finished.connect(lambda result: self._process_finished(result, "分水岭分割")) thread.start() def calculate_hu_moments(self): """计算Hu不变矩""" if self.processed_image is None: return def calculate_moments(image): if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 计算矩 moments = cv2.moments(gray) # 计算Hu不变矩 hu_moments = cv2.HuMoments(moments) # 对数变换,方便显示 for i in range(7): hu_moments[i] = -1 * np.copysign(1.0, hu_moments[i]) * np.log10(np.abs(hu_moments[i])) return hu_moments thread = ImageProcessingThread(calculate_moments, self.processed_image) thread.finished.connect(self._on_hu_moments_calculated) thread.start() def _on_hu_moments_calculated(self, hu_moments): """Hu不变矩计算完成后的回调""" result = "Hu不变矩:\n\n" for i, moment in enumerate(hu_moments): result += f"H{i + 1}: {moment[0]:.8f}\n" self.info_text.setText(result) self.history_tabs.setCurrentWidget(self.info_tab) self.statusBar().showMessage("Hu不变矩计算完成") def calculate_glcm(self): """计算灰度共生矩阵""" if self.processed_image is None: return thread = ImageProcessingThread(self._calculate_glcm_thread, self.processed_image) thread.finished.connect(self._on_glcm_calculated) thread.start() def _calculate_glcm_thread(self, image): """在线程中计算GLCM""" if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image.copy() # 量化到16级灰度以减少计算量 gray_quantized = (gray // 16).astype(np.uint8) # 计算GLCM矩阵 (距离=1, 角度=0, 45, 90, 135度) glcm = graycomatrix(gray_quantized, distances=[1], angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], levels=16, symmetric=True, normed=True) return glcm def _on_glcm_calculated(self, glcm): """GLCM计算完成后的回调""" self.glcm_widget.update_glcm(self.processed_image) self.history_tabs.setCurrentWidget(self.glcm_tab) self.statusBar().showMessage("灰度共生矩阵计算完成") def _process_finished(self, result, operation_name): """图像处理完成后的回调""" self.processed_image = result self.display_image(result) self.add_to_history(result, operation_name) self.statusBar().showMessage(f"{operation_name}完成") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec())为什么运行后打开图片D:\python\python.exe D:\PythonProject\.venv\2.py 插入图片后闪退后显示 进程已结束,退出代码为 -1073740791 (0xC0000409)
06-09
import os import sys import tempfile import tkinter as tk from tkinter import filedialog, messagebox, colorchooser, ttk from io import BytesIO import re from datetime import datetime import traceback # 添加缺失的导入 import fitz # PyMuPDF class PDFDateHighlighter: def __init__(self, root): self.root = root self.root.title("PDF日期高亮打印工具") self.root.geometry("1200x700") # 增大窗口以适应两页显示 # 指定日期列表 self.specified_dates = [ "2025-01-01", "2025-01-27", "2025-01-28", "2025-01-29", "2025-03-28", "2025-03-29", "2025-03-31", "2025-04-01", "2025-04-02", "2025-04-03", "2025-04-04", "2025-04-07", "2025-04-18", "2025-04-20", "2025-05-01", "2025-05-12", "2025-05-13", "2025-05-29", "2025-05-30", "2025-06-01", "2025-06-06", "2025-06-09", "2025-06-27", "2025-08-17", "2025-09-05", "2025-12-25", "2025-12-26" ] self.pdf_path = None self.highlighted_pdf_path = None self.current_page = 0 self.total_pages = 0 self.pdf_document = None # 使用荧光颜色 self.highlight_color = (1.0, 1.0, 0.0) # 荧光黄色 RGB self.highlight_alpha = 50 # 降低不透明度以确保文字可见 self.current_images = [] # 保持对图像的引用(两页) # 手动涂改相关变量 self.drawing_mode = False self.erasing_mode = False self.start_x = None self.start_y = None self.brush_width = 10 # 默认笔宽 self.drawing_items = [] # 存储绘制的项目用于撤回 self.current_drawing = [] # 当前绘制的项目 self.setup_ui() def setup_ui(self): # 主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置行列权重 self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(3, weight=1) # 文件选择区域 ttk.Label(main_frame, text="PDF文件:").grid(row=0, column=0, sticky=tk.W, pady=5) self.file_path = ttk.Entry(main_frame, width=80) self.file_path.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5) ttk.Button(main_frame, text="浏览", command=self.browse_file).grid(row=0, column=2, pady=5) # 按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.grid(row=1, column=0, columnspan=3, pady=10) # 添加撤回按钮 ttk.Button(btn_frame, text="撤回", command=self.undo_drawing).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="处理PDF", command=self.process_pdf).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="上一页", command=self.prev_page).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="下一页", command=self.next_page).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="打印", command=self.print_pdf).pack(side=tk.LEFT, padx=5) # 颜色选择按钮和图标 ttk.Button(btn_frame, text="选择高亮颜色", command=self.choose_color).pack(side=tk.LEFT, padx=5) # 颜色图标 self.color_icon = tk.Canvas(btn_frame, width=20, height=20, bg="#ffff00") # 荧光黄色 self.color_icon.pack(side=tk.LEFT, padx=5) self.color_icon.bind("<Button-1>", lambda e: self.choose_color()) # 手动涂改按钮 ttk.Button(btn_frame, text="手动涂改", command=self.toggle_drawing).pack(side=tk.LEFT, padx=5) # 笔宽设置 ttk.Label(btn_frame, text="笔宽:").pack(side=tk.LEFT, padx=5) self.brush_width_var = tk.StringVar(value="15") brush_width_spin = ttk.Spinbox(btn_frame, from_=1, to=20, width=5, textvariable=self.brush_width_var) brush_width_spin.pack(side=tk.LEFT, padx=5) brush_width_spin.bind("<<Increment>>", self.update_brush_width) brush_width_spin.bind("<<Decrement>>", self.update_brush_width) # 橡皮擦按钮 ttk.Button(btn_frame, text="橡皮擦", command=self.toggle_erasing).pack(side=tk.LEFT, padx=5) # 页面显示区域 self.page_label = ttk.Label(main_frame, text="页面: 0/0") self.page_label.grid(row=2, column=0, columnspan=3, pady=5) # PDF显示区域 - 改为两页平行显示 self.pdf_frame = ttk.Frame(main_frame) self.pdf_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10) self.pdf_frame.columnconfigure(0, weight=1) self.pdf_frame.columnconfigure(1, weight=1) self.pdf_frame.rowconfigure(0, weight=1) # 创建两个Canvas用于显示两页 self.canvas1 = tk.Canvas(self.pdf_frame, bg="white") self.canvas2 = tk.Canvas(self.pdf_frame, bg="white") # 添加滚动条 - 改为快速翻页滚动条 self.page_scrollbar = ttk.Scrollbar(self.pdf_frame, orient=tk.VERTICAL) self.h_scrollbar = ttk.Scrollbar(self.pdf_frame, orient=tk.HORIZONTAL) # 配置Canvas和滚动条 self.canvas1.configure(xscrollcommand=self.h_scrollbar.set) self.canvas2.configure(xscrollcommand=self.h_scrollbar.set) self.h_scrollbar.configure(command=self.sync_scroll_x) # 配置页面滚动条 self.page_scrollbar.configure(command=self.scroll_pages) # 布局 self.canvas1.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5)) self.canvas2.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0)) self.page_scrollbar.grid(row=0, column=2, sticky=(tk.N, tk.S)) self.h_scrollbar.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E)) # 绑定鼠标滚轮事件 self.canvas1.bind("<MouseWheel>", self.on_mousewheel) self.canvas2.bind("<MouseWheel>", self.on_mousewheel) self.canvas1.bind("<Button-4>", self.on_mousewheel) # Linux向上滚动 self.canvas2.bind("<Button-4>", self.on_mousewheel) self.canvas1.bind("<Button-5>", self.on_mousewheel) # Linux向下滚动 self.canvas2.bind("<Button-5>", self.on_mousewheel) # 绑定手动涂改事件 self.canvas1.bind("<ButtonPress-1>", self.start_drawing) self.canvas1.bind("<B1-Motion>", self.draw) self.canvas1.bind("<ButtonRelease-1>", self.stop_drawing) self.canvas2.bind("<ButtonPress-1>", self.start_drawing) self.canvas2.bind("<B1-Motion>", self.draw) self.canvas2.bind("<ButtonRelease-1>", self.stop_drawing) # 状态栏 self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W) self.status_bar.grid(row=4, column=0, sticky=(tk.W, tk.E)) def sync_scroll_x(self, *args): """同步两个Canvas的水平滚动""" self.canvas1.xview(*args) self.canvas2.xview(*args) def scroll_pages(self, *args): """处理页面滚动条事件""" if not self.pdf_document or self.total_pages == 0: return # 获取滚动条位置 if len(args) > 1 and args[0] == "moveto": scroll_pos = float(args[1]) elif len(args) > 2 and args[0] == "scroll": units = int(args[1]) scroll_pos = float(args[2]) else: return # 计算目标页面 target_page = int(scroll_pos * (self.total_pages - 1)) # 确保目标页面是偶数(因为每次显示两页) if target_page % 2 != 0: target_page = max(0, target_page - 1) # 更新当前页面 if target_page != self.current_page: self.current_page = target_page self.display_page() def update_scrollbar(self): """更新滚动条位置""" if not self.pdf_document or self.total_pages == 0: return # 计算滚动条位置 scroll_pos = self.current_page / (self.total_pages - 1) if self.total_pages > 1 else 0 # 更新滚动条 self.page_scrollbar.set(scroll_pos, scroll_pos + 1/(self.total_pages)) def on_mousewheel(self, event): """处理鼠标滚轮事件""" if event.num == 4 or event.delta > 0: # 向上滚动 self.prev_page() elif event.num == 5 or event.delta < 0: # 向下滚动 self.next_page() def toggle_drawing(self): """切换手动涂改模式""" self.drawing_mode = not self.drawing_mode self.erasing_mode = False # 确保橡皮擦模式关闭 if self.drawing_mode: self.status_bar.config(text="手动涂改模式已启用 - 点击并拖动以涂改") else: self.status_bar.config(text="手动涂改模式已禁用") def toggle_erasing(self): """切换橡皮擦模式""" self.erasing_mode = not self.erasing_mode self.drawing_mode = False # 确保涂改模式关闭 if self.erasing_mode: self.status_bar.config(text="橡皮擦模式已启用 - 点击并拖动以擦除") else: self.status_bar.config(text="橡皮擦模式已禁用") def update_brush_width(self, event): """更新笔宽""" try: self.brush_width = int(self.brush_width_var.get()) except ValueError: self.brush_width = 5 self.brush_width_var.set("5") def start_drawing(self, event): """开始手动涂改""" if not self.drawing_mode and not self.erasing_mode: return self.start_x = event.x self.start_y = event.y self.current_drawing = [] def draw(self, event): """手动涂改或擦除""" if (not self.drawing_mode and not self.erasing_mode) or self.start_x is None or self.start_y is None: return # 获取当前Canvas current_canvas = event.widget # 确定颜色 - 涂改模式使用荧光色,擦除模式使用白色 if self.drawing_mode: # 使用荧光色,但降低不透明度以确保文字可见 color = "#{:02x}{:02x}{:02x}".format( int(self.highlight_color[0] * 255), int(self.highlight_color[1] * 255), int(self.highlight_color[2] * 255) ) # 创建半透明矩形 - 使用点状图案实现半透明效果 rect_id = current_canvas.create_rectangle( self.start_x, self.start_y, event.x, event.y, fill=color, outline="", stipple="gray50" # 使用点状图案实现半透明效果 ) self.current_drawing.append(rect_id) else: # 擦除模式 # 查找并删除橡皮擦重叠的手动涂改 items = current_canvas.find_overlapping( event.x - self.brush_width, event.y - self.brush_width, event.x + self.brush_width, event.y + self.brush_width ) for item in items: if item in self.get_all_drawing_items(): current_canvas.delete(item) # 从所有存储的绘制项目中移除 self.remove_drawing_item(item) self.start_x = event.x self.start_y = event.y def get_all_drawing_items(self): """获取所有绘制项目的ID""" all_items = [] for drawing in self.drawing_items: all_items.extend(drawing) return all_items def remove_drawing_item(self, item_id): """从存储的绘制项目中移除指定ID""" for i, drawing in enumerate(self.drawing_items): if item_id in drawing: drawing.remove(item_id) if not drawing: # 如果绘图为空,移除整个绘图 self.drawing_items.pop(i) break def stop_drawing(self, event): """停止手动涂改""" if self.current_drawing and self.drawing_mode: # 只在涂改模式下保存绘制项目 self.drawing_items.append(self.current_drawing.copy()) self.start_x = None self.start_y = None self.current_drawing = [] def undo_drawing(self): """撤回最后一次涂改""" if not self.drawing_items: return # 获取最后一次涂改的项目 last_drawing = self.drawing_items.pop() # 从两个Canvas中删除这些项目 for item_id in last_drawing: self.canvas1.delete(item_id) self.canvas2.delete(item_id) self.status_bar.config(text="已撤回最后一次涂改") def choose_color(self): """允许用户选择高亮颜色""" color = colorchooser.askcolor(title="选择高亮颜色", initialcolor="#ffff00") if color[0]: # 用户选择了颜色 r, g, b = color[0] self.highlight_color = (r/255, g/255, b/255) # 转换为0-1范围 # 更新颜色图标 self.color_icon.config(bg=color[1]) def browse_file(self): try: file_path = filedialog.askopenfilename(filetypes=[("PDF文件", "*.pdf")]) if file_path: self.file_path.delete(0, tk.END) self.file_path.insert(0, file_path) self.pdf_path = file_path self.load_pdf() except Exception as e: messagebox.showerror("错误", f"浏览文件时发生错误: {str(e)}") def load_pdf(self): if not self.pdf_path: return try: if self.pdf_document: self.pdf_document.close() self.pdf_document = fitz.open(self.pdf_path) self.total_pages = len(self.pdf_document) self.current_page = 0 self.display_page() self.update_scrollbar() except Exception as e: messagebox.showerror("错误", f"无法打开PDF文件: {str(e)}") def display_page(self): if not self.pdf_document or self.total_pages == 0: return self.page_label.config(text=f"页面: {self.current_page+1}-{min(self.current_page+2, self.total_pages)}/{self.total_pages}") try: # 清空当前图像引用 self.current_images = [] # 显示第一页 if self.current_page < self.total_pages: page1 = self.pdf_document[self.current_page] zoom = 1.0 # 缩小一点以适应两页显示 mat = fitz.Matrix(zoom, zoom) pix1 = page1.get_pixmap(matrix=mat) # 转换为PhotoImage img_data1 = pix1.tobytes("ppm") img1 = tk.PhotoImage(data=img_data1) self.current_images.append(img1) # 保持引用 # 更新Canvas self.canvas1.config(width=min(img1.width(), 550), height=min(img1.height(), 500)) self.canvas1.delete("all") self.canvas1.create_image(0, 0, anchor=tk.NW, image=img1) # 设置滚动区域 self.canvas1.config(scrollregion=self.canvas1.bbox(tk.ALL)) # 显示第二页(如果有) if self.current_page + 1 < self.total_pages: page2 = self.pdf_document[self.current_page + 1] zoom = 1.0 # 缩小一点以适应两页显示 mat = fitz.Matrix(zoom, zoom) pix2 = page2.get_pixmap(matrix=mat) # 转换为PhotoImage img_data2 = pix2.tobytes("ppm") img2 = tk.PhotoImage(data=img_data2) self.current_images.append(img2) # 保持引用 # 更新Canvas self.canvas2.config(width=min(img2.width(), 550), height=min(img2.height(), 500)) self.canvas2.delete("all") self.canvas2.create_image(0, 0, anchor=tk.NW, image=img2) # 设置滚动区域 self.canvas2.config(scrollregion=self.canvas2.bbox(tk.ALL)) else: # 如果没有第二页,清空第二个Canvas self.canvas2.delete("all") self.canvas2.config(width=0, height=0) # 更新滚动条位置 self.update_scrollbar() except Exception as e: messagebox.showerror("错误", f"显示页面时发生错误: {str(e)}") def prev_page(self): if self.current_page > 0: self.current_page = max(0, self.current_page - 2) self.display_page() def next_page(self): if self.pdf_document and self.current_page + 2 < self.total_pages: self.current_page += 2 self.display_page() def process_pdf(self): if not self.pdf_path: messagebox.showwarning("警告", "请先选择PDF文件") return self.status_bar.config(text="正在处理PDF...") self.root.update() try: # 创建临时文件保存处理后的PDF temp_file = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) self.highlighted_pdf_path = temp_file.name temp_file.close() # 打开原始PDF doc = fitz.open(self.pdf_path) # 遍历每一页 for page_num in range(len(doc)): page = doc[page_num] # 获取页面文本 - 使用更精确的搜索方法 text = page.get_text("text") # 查找所有日期格式的文本 date_pattern = r"\d{4}-\d{2}-\d{2}" dates = re.findall(date_pattern, text) print(f"在第 {page_num+1} 页找到的日期: {dates}") # 调试信息 # 对于每个找到的日期,查找其在页面上的位置并高亮 for date_str in dates: if self.should_highlight(date_str): print(f"高亮日期: {date_str}") # 调试信息 # 搜索日期文本的位置 text_instances = page.search_for(date_str) print(f"找到 {len(text_instances)} 个匹配项") # 调试信息 for inst in text_instances: # 使用高亮注释 highlight = page.add_highlight_annot(inst) # 设置荧光颜色 highlight.set_colors(stroke=self.highlight_color) highlight.set_opacity(self.highlight_alpha) highlight.update() # 保存处理后的PDF doc.save(self.highlighted_pdf_path) doc.close() # 重新加载处理后的PDF if self.pdf_document: self.pdf_document.close() self.pdf_document = fitz.open(self.highlighted_pdf_path) self.total_pages = len(self.pdf_document) self.current_page = 0 self.display_page() self.status_bar.config(text="处理完成") messagebox.showinfo("成功", "PDF处理完成,日期已高亮显示") except Exception as e: self.status_bar.config(text="处理失败") error_msg = f"处理PDF时发生错误: {str(e)}\n\n详细信息:\n{traceback.format_exc()}" messagebox.showerror("错误", error_msg) def should_highlight(self, date_str): # 检查是否为指定日期 if date_str in self.specified_dates: return True # 检查是否为周日 try: date_obj = datetime.strptime(date_str, "%Y-%m-%d") if date_obj.weekday() == 6: # 周日是6 return True except ValueError: pass return False def print_pdf(self): if not self.highlighted_pdf_path or not os.path.exists(self.highlighted_pdf_path): messagebox.showwarning("警告", "请先处理PDF文件") return try: # 使用系统默认程序打开PDF进行打印 if sys.platform == "win32": os.startfile(self.highlighted_pdf_path, "print") elif sys.platform == "darwin": # macOS os.system(f"lpr '{self.highlighted_pdf_path}'") else: # Linux os.system(f"xdg-open '{self.highlighted_pdf_path}'") self.status_bar.config(text="打印任务已发送") except Exception as e: messagebox.showerror("错误", f"打印失败: {str(e)}") def __del__(self): # 清理临时文件 if self.highlighted_pdf_path and os.path.exists(self.highlighted_pdf_path): try: os.unlink(self.highlighted_pdf_path) except: pass # 关闭PDF文档 if self.pdf_document: try: self.pdf_document.close() except: pass def main(): try: # 检查是否安装了PyMuPDF try: import fitz except ImportError: messagebox.showerror("缺少依赖", "请安装PyMuPDF库: pip install PyMuPDF") return root = tk.Tk() app = PDFDateHighlighter(root) root.mainloop() except Exception as e: error_msg = f"程序发生未预期错误: {str(e)}\n\n详细信息:\n{traceback.format_exc()}" messagebox.showerror("严重错误", error_msg) if __name__ == "__main__": main() 手动涂改时涂改的粗细未按笔宽输入的值增粗或减细,涂改的笔宽默认为10mm 点击“处理PDF”按钮后未自动对PDF中所有的图片进行扫描并拾取查找对应的日期及周日日期进行自动涂上颜色,如“2025-06-27”未被自动涂改上颜色
08-22
import os import sys import tempfile import tkinter as tk from tkinter import filedialog, messagebox, colorchooser, ttk from io import BytesIO import re from datetime import datetime import traceback import shutil import subprocess # 尝试导入win32print,如果不可用则提供替代方案 try: import win32print # 用于获取打印机列表 WIN32PRINT_AVAILABLE = True except ImportError: WIN32PRINT_AVAILABLE = False print("win32print模块不可用,将使用默认打印机") # 添加缺失的导入 import fitz # PyMuPDF # 尝试导入OCR相关库 try: from pdf2image import convert_from_path import pytesseract from PIL import Image, ImageDraw, ImageFont # 检查Tesseract OCR是否可用 try: pytesseract.get_tesseract_version() OCR_AVAILABLE = True except: OCR_AVAILABLE = False print("Tesseract OCR未正确安装或配置") except ImportError: OCR_AVAILABLE = False class PDFDateHighlighter: def __init__(self, root): self.root = root self.root.title("PDF日期高亮打印工具") self.root.geometry("1200x700") # 增大窗口以适应两页显示 # 指定日期列表 - 已更新 self.specified_dates = [ "2025-01-01", "2025-01-27", "2025-01-29", "2025-03-29", "2025-03-31", "2025-04-01", "2025-04-18", "2025-04-20", "2025-05-01", "2025-05-12", "2025-05-29", "2025-06-01", "2025-06-06", "2025-06-27", "2025-08-17", "2025-09-05", "2025-12-25" ] self.pdf_path = None self.highlighted_pdf_path = None self.current_page = 0 self.total_pages = 0 self.pdf_document = None # 使用不同的颜色作为默认值 self.friday_color = (0.0, 1.0, 0.0) # 周五颜色为绿色 self.sunday_color = (1.0, 0.0, 0.0) # 周日颜色为红色 self.other_red_color = (1.0, 0.0, 0.0) # 指定红日颜色为红色 self.highlight_alpha = 50 # 降低不透明度以确保文字可见 self.current_images = [] # 保持对图像的引用(两页) # 手动涂改相关变量 self.drawing_mode = False self.erasing_mode = False # 擦除模式 self.start_x = None self.start_y = None self.brush_width = 15 # 默认笔宽改为15mm self.drawing_items = [] # 存储绘制的项目用于撤回 self.current_drawing = [] # 当前绘制的项目 self.drawing_color = (1.0, 0.0, 0.0) # 涂改颜色,默认为红色 self.setup_ui() def setup_ui(self): # 主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置行列权重 self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(3, weight=1) # 文件选择区域 ttk.Label(main_frame, text="PDF文件:").grid(row=0, column=0, sticky=tk.W, pady=5) self.file_path = ttk.Entry(main_frame, width=80) self.file_path.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5, padx=5) ttk.Button(main_frame, text="浏览", command=self.browse_file).grid(row=0, column=2, pady=5) # 按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.grid(row=1, column=0, columnspan=3, pady=10) # 调整按钮顺序:将PDF转COR按钮放在撤回和处理PDF按钮之间 ttk.Button(btn_frame, text="保存为PDF", command=self.save_as_pdf).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="撤回", command=self.undo_drawing).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="PDF转COR", command=self.pdf_to_cor).pack(side=tk.LEFT, padx=5) # 调整位置 ttk.Button(btn_frame, text="处理PDF", command=self.process_pdf).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="上一页", command=self.prev_page).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="下一页", command=self.next_page).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="打印", command=self.show_print_dialog).pack(side=tk.LEFT, padx=5) # 颜色选择按钮和图标 - 修改为三个按钮 ttk.Button(btn_frame, text="周五颜色", command=self.choose_friday_color).pack(side=tk.LEFT, padx=5) self.friday_icon = tk.Canvas(btn_frame, width=20, height=20, bg="#00ff00") # 绿色 self.friday_icon.pack(side=tk.LEFT, padx=5) self.friday_icon.bind("<Button-1>", lambda e: self.choose_friday_color()) ttk.Button(btn_frame, text="周日颜色", command=self.choose_sunday_color).pack(side=tk.LEFT, padx=5) self.sunday_icon = tk.Canvas(btn_frame, width=20, height=20, bg="#ff0000") # 红色 self.sunday_icon.pack(side=tk.LEFT, padx=5) self.sunday_icon.bind("<Button-1>", lambda e: self.choose_sunday_color()) ttk.Button(btn_frame, text="指定红日", command=self.choose_other_red_color).pack(side=tk.LEFT, padx=5) self.other_red_icon = tk.Canvas(btn_frame, width=20, height=20, bg="#ff0000") # 红色 self.other_red_icon.pack(side=tk.LEFT, padx=5) self.other_red_icon.bind("<Button-1>", lambda e: self.choose_other_red_color()) ttk.Button(btn_frame, text="涂改颜色", command=self.choose_drawing_color).pack(side=tk.LEFT, padx=5) self.drawing_icon = tk.Canvas(btn_frame, width=20, height=20, bg="#ff0000") # 红色 self.drawing_icon.pack(side=tk.LEFT, padx=5) self.drawing_icon.bind("<Button-1>", lambda e: self.choose_drawing_color()) # 手动涂改按钮 ttk.Button(btn_frame, text="手动涂改", command=self.toggle_drawing).pack(side=tk.LEFT, padx=5) # 擦除按钮 ttk.Button(btn_frame, text="擦除", command=self.toggle_erasing).pack(side=tk.LEFT, padx=5) # 笔宽设置 ttk.Label(btn_frame, text="笔宽:").pack(side=tk.LEFT, padx=5) self.brush_width_var = tk.StringVar(value="15") brush_width_spin = ttk.Spinbox(btn_frame, from_=1, to=50, width=5, textvariable=self.brush_width_var) brush_width_spin.pack(side=tk.LEFT, padx=5) brush_width_spin.bind("<<Increment>>", self.update_brush_width) brush_width_spin.bind("<<Decrement>>", self.update_brush_width) brush_width_spin.bind("<FocusOut>", self.update_brush_width) # 透明度设置 ttk.Label(btn_frame, text="透明度:").pack(side=tk.LEFT, padx=5) self.alpha_var = tk.StringVar(value="50") alpha_spin = ttk.Spinbox(btn_frame, from_=10, to=90, width=5, textvariable=self.alpha_var) alpha_spin.pack(side=tk.LEFT, padx=5) alpha_spin.bind("<<Increment>>", self.update_alpha) alpha_spin.bind("<<Decrement>>", self.update_alpha) alpha_spin.bind("<FocusOut>", self.update_alpha) # 页面显示区域 self.page_label = ttk.Label(main_frame, text="页面: 0/0") self.page_label.grid(row=2, column=0, columnspan=3, pady=5) # PDF显示区域 - 改为两页平行显示 self.pdf_frame = ttk.Frame(main_frame) self.pdf_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10) self.pdf_frame.columnconfigure(0, weight=1) self.pdf_frame.columnconfigure(1, weight=1) self.pdf_frame.rowconfigure(0, weight=1) # 创建两个Canvas用于显示两页 self.canvas1 = tk.Canvas(self.pdf_frame, bg="white") self.canvas2 = tk.Canvas(self.pdf_frame, bg="white") # 添加滚动条 - 改为快速翻页滚动条 self.page_scrollbar = ttk.Scrollbar(self.pdf_frame, orient=tk.VERTICAL) self.h_scrollbar = ttk.Scrollbar(self.pdf_frame, orient=tk.HORIZONTAL) # 配置Canvas和滚动条 self.canvas1.configure(xscrollcommand=self.h_scrollbar.set) self.canvas2.configure(xscrollcommand=self.h_scrollbar.set) self.h_scrollbar.configure(command=self.sync_scroll_x) # 配置页面滚动条 self.page_scrollbar.configure(command=self.scroll_pages) # 布局 self.canvas1.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5)) self.canvas2.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0)) self.page_scrollbar.grid(row=0, column=2, sticky=(tk.N, tk.S)) self.h_scrollbar.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E)) # 绑定鼠标滚轮事件 self.canvas1.bind("<MouseWheel>", self.on_mousewheel) self.canvas2.bind("<MouseWheel>", self.on_mousewheel) self.canvas1.bind("<Button-4>", self.on_mousewheel) # Linux向上滚动 self.canvas2.bind("<Button-4>", self.on_mousewheel) self.canvas1.bind("<Button-5>", self.on_mousewheel) # Linux向下滚动 self.canvas2.bind("<Button-5>", self.on_mousewheel) # 绑定手动涂改事件 self.canvas1.bind("<ButtonPress-1>", self.start_drawing) self.canvas1.bind("<B1-Motion>", self.draw) self.canvas1.bind("<ButtonRelease-1>", self.stop_drawing) self.canvas2.bind("<ButtonPress-1>", self.start_drawing) self.canvas2.bind("<B1-Motion>", self.draw) self.canvas2.bind("<ButtonRelease-1>", self.stop_drawing) # 状态栏 self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W) self.status_bar.grid(row=4, column=0, sticky=(tk.W, tk.E)) def pdf_to_cor(self): """将PDF转换为可搜索的文本PDF(OCR处理)""" if not self.pdf_path: messagebox.showwarning("警告", "请先选择PDF文件") return if not OCR_AVAILABLE: messagebox.showerror("错误", "OCR功能不可用。请确保已安装:\n1. Tesseract OCR\n2. pdf2image库\n3. Poppler工具") return try: # 获取桌面路径 if os.name == 'nt': # Windows desktop_path = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop') else: # macOS 或 Linux desktop_path = os.path.join(os.path.join(os.expanduser('~')), 'Desktop') # 生成默认文件名 base_name = os.path.basename(self.pdf_path) default_filename = os.path.splitext(base_name)[0] + "_ocr.pdf" default_path = os.path.join(desktop_path, default_filename) # 询问保存路径 save_path = filedialog.asksaveasfilename( defaultextension=".pdf", filetypes=[("PDF文件", "*.pdf")], initialdir=desktop_path, initialfile=default_filename ) if not save_path: return self.status_bar.config(text="正在转换PDF到可搜索文本PDF...") self.root.update() # 创建进度窗口 progress_window = tk.Toplevel(self.root) progress_window.title("OCR处理进度") progress_window.geometry("400x100") progress_window.transient(self.root) progress_window.grab_set() progress_label = ttk.Label(progress_window, text="正在处理第 0 页,共 0 页") progress_label.pack(pady=10) progress_bar = ttk.Progressbar(progress_window, mode='determinate') progress_bar.pack(fill=tk.X, padx=20, pady=10) progress_window.update() # 打开原始PDF doc = fitz.open(self.pdf_path) total_pages = len(doc) # 创建新的PDF文档 output_doc = fitz.open() # 设置进度 progress_bar["maximum"] = total_pages progress_bar["value"] = 0 # 临时目录用于存储图像 with tempfile.TemporaryDirectory() as temp_dir: # 将PDF转换为图像 images = convert_from_path( self.pdf_path, dpi=300, # 高DPI以获得更好的OCR结果 output_folder=temp_dir, fmt='jpeg', thread_count=4, use_pdftocairo=True ) for page_num, image in enumerate(images): # 更新进度 progress_bar["value"] = page_num + 1 progress_label.config(text=f"正在处理第 {page_num + 1} 页,共 {total_pages} 页") progress_window.update() # 使用Tesseract进行OCR pdf_bytes = pytesseract.image_to_pdf_or_hocr( image, extension='pdf', config='--psm 6 -c preserve_interword_spaces=1' ) # 将OCR结果转换为fitz文档 ocr_pdf = fitz.open("pdf", pdf_bytes) # 将OCR页面添加到输出文档 output_doc.insert_pdf(ocr_pdf) # 关闭OCR PDF ocr_pdf.close() # 保存输出文档 output_doc.save(save_path) output_doc.close() doc.close() # 关闭进度窗口 progress_window.destroy() self.status_bar.config(text=f"PDF已成功转换为可搜索文本PDF: {save_path}") messagebox.showinfo("成功", f"PDF文件已成功转换为可搜索文本PDF:\n{save_path}") except Exception as e: error_msg = f"转换PDF到可搜索文本PDF时发生错误: {str(e)}\n\n详细信息:\n{traceback.format_exc()}" messagebox.showerror("错误", error_msg) self.status_bar.config(text="转换失败") def choose_friday_color(self): """选择周五颜色""" color = colorchooser.askcolor(title="选择周五颜色", initialcolor="#00ff00") if color[0]: # 用户选择了颜色 r, g, b = color[0] self.friday_color = (r/255, g/255, b/255) # 转换为0-1范围 # 更新颜色图标 self.friday_icon.config(bg=color[1]) def choose_sunday_color(self): """选择周日颜色""" color = colorchooser.askcolor(title="选择周日颜色", initialcolor="#ff0000") if color[0]: # 用户选择了颜色 r, g, b = color[0] self.sunday_color = (r/255, g/255, b/255) # 转换为0-1范围 # 更新颜色图标 self.sunday_icon.config(bg=color[1]) def choose_other_red_color(self): """选择指定红日颜色""" color = colorchooser.askcolor(title="选择指定红日颜色", initialcolor="#ff0000") if color[0]: # 用户选择了颜色 r, g, b = color[0] self.other_red_color = (r/255, g/255, b/255) # 转换为0-1范围 # 更新颜色图标 self.other_red_icon.config(bg=color[1]) def choose_drawing_color(self): """选择涂改颜色""" color = colorchooser.askcolor(title="选择涂改颜色", initialcolor="#ff0000") if color[0]: # 用户选择了颜色 r, g, b = color[0] self.drawing_color = (r/255, g/255, b/255) # 转换为0-1范围 # 更新颜色图标 self.drawing_icon.config(bg=color[1]) def save_as_pdf(self): """保存PDF文件""" if not self.highlighted_pdf_path or not os.path.exists(self.highlighted_pdf_path): messagebox.showwarning("警告", "请先处理PDF文件") return try: # 获取桌面路径 if os.name == 'nt': # Windows desktop_path = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop') else: # macOS 或 Linux desktop_path = os.path.join(os.path.join(os.expanduser('~')), 'Desktop') # 生成默认文件名 - 修复: 使用os.path.splitext而不是os.path.splicext base_name = os.path.basename(self.pdf_path) default_filename = os.path.splitext(base_name)[0] + "_highlighted.pdf" default_path = os.path.join(desktop_path, default_filename) # 询问保存路径 save_path = filedialog.asksaveasfilename( defaultextension=".pdf", filetypes=[("PDF文件", "*.pdf")], initialdir=desktop_path, initialfile=default_filename ) if save_path: # 复制文件 shutil.copy2(self.highlighted_pdf_path, save_path) self.status_bar.config(text=f"PDF已保存到: {save_path}") messagebox.showinfo("成功", f"PDF文件已保存到:\n{save_path}") except Exception as e: messagebox.showerror("错误", f"保存PDF时发生错误: {str(e)}") def toggle_erasing(self): """切换擦除模式""" self.erasing_mode = not self.erasing_mode self.drawing_mode = False # 确保涂改模式关闭 if self.erasing_mode: self.status_bar.config(text="擦除模式已启用 - 点击并拖动以擦除手动涂改") else: self.status_bar.config(text="就绪") def sync_scroll_x(self, *args): """同步两个Canvas的水平滚动""" self.canvas1.xview(*args) self.canvas2.xview(*args) def scroll_pages(self, *args): """处理页面滚动条事件""" if not self.pdf_document or self.total_pages == 0: return # 获取滚动条位置 if len(args) > 1 and args[0] == "moveto": scroll_pos = float(args[1]) elif len(args) > 2 and args[0] == "scroll": units = int(args[1]) scroll_pos = float(args[2]) else: return # 计算目标页面 target_page = int(scroll_pos * (self.total_pages - 1)) # 确保目标页面是偶数(因为每次显示两页) if target_page % 2 != 0: target_page = max(0, target_page - 1) # 更新当前页面 if target_page != self.current_page: self.current_page = target_page self.display_page() def update_scrollbar(self): """更新滚动条位置""" if not self.pdf_document or self.total_pages == 0: return # 计算滚动条位置 scroll_pos = self.current_page / (self.total_pages - 1) if self.total_pages > 1 else 0 # 更新滚动条 self.page_scrollbar.set(scroll_pos, scroll_pos + 1/(self.total_pages)) def on_mousewheel(self, event): """处理鼠标滚轮事件""" if event.num == 4 or event.delta > 0: # 向上滚动 self.prev_page() elif event.num == 5 or event.delta < 0: # 向下滚动 self.next_page() def toggle_drawing(self): """切换手动涂改模式""" self.drawing_mode = not self.drawing_mode self.erasing_mode = False # 确保擦除模式关闭 if self.drawing_mode: self.status_bar.config(text="手动涂改模式已启用 - 点击并拖动以涂改") else: self.status_bar.config(text="就绪") def update_brush_width(self, event): """更新笔宽""" try: self.brush_width = int(self.brush_width_var.get()) except ValueError: self.brush_width = 15 self.brush_width_var.set("15") def update_alpha(self, event): """更新透明度""" try: self.highlight_alpha = int(self.alpha_var.get()) except ValueError: self.highlight_alpha = 50 self.alpha_var.set("50") def start_drawing(self, event): """开始手动涂改或擦除""" if not (self.drawing_mode or self.erasing_mode): return self.start_x = event.x self.start_y = event.y self.current_drawing = [] def draw(self, event): """手动涂改或擦除""" if (not self.drawing_mode and not self.erasing_mode) or self.start_x is None or self.start_y is None: return # 获取当前Canvas current_canvas = event.widget if self.drawing_mode: # 使用涂改颜色,确保使用透明效果 r, g, b = self.drawing_color # 创建半透明颜色 - 使用alpha混合公式 # 背景色为白色(255,255,255),前景色为涂改颜色 alpha_factor = self.highlight_alpha / 100 r_mixed = int((r * alpha_factor + 1.0 * (1 - alpha_factor)) * 255) g_mixed = int((g * alpha_factor + 1.0 * (1 - alpha_factor)) * 255) b_mixed = int((b * alpha_factor + 1.0 * (1 - alpha_factor)) * 255) color = "#{:02x}{:02x}{:02x}".format(r_mixed, g_mixed, b_mixed) # 使用椭圆形实现圆形笔刷效果 half_width = self.brush_width # 创建荧光笔效果 - 使用点状图案实现透明效果 oval_id = current_canvas.create_oval( event.x - half_width, event.y - half_width, event.x + half_width, event.y + half_width, fill=color, outline="", stipple="gray50" # 使用点状图案实现透明效果 ) self.current_drawing.append(oval_id) # 如果移动距离较大,在两点之间绘制线条填充间隙 distance = ((event.x - self.start_x)**2 + (event.y - self.start_y)**2)**0.5 if distance > self.brush_width: line_id = current_canvas.create_line( self.start_x, self.start_y, event.x, event.y, width=self.brush_width*2, fill=color, capstyle=tk.ROUND, joinstyle=tk.ROUND, stipple="gray50" # 使用点状图案实现透明效果 ) self.current_drawing.append(line_id) elif self.erasing_mode: # 擦除模式 - 查找并删除当前位置重叠的手动涂改项目 half_width = self.brush_width # 获取当前位置附近的所有项目 items = current_canvas.find_overlapping( event.x - half_width, event.y - half_width, event.x + half_width, event.y + half_width ) # 只删除手动涂改的项目(在drawing_items中的项目) all_drawing_items = self.get_all_drawing_items() for item in items: if item in all_drawing_items: current_canvas.delete(item) # 从drawing_items中移除 for drawing in self.drawing_items: if item in drawing: drawing.remove(item) # 如果移动距离较大,在两点之间查找更多项目 distance = ((event.x - self.start_x)**2 + (event.y - self.start_y)**2)**0.5 if distance > self.brush_width: # 计算线段上的多个点 steps = int(distance / (self.brush_width / 2)) + 1 for i in range(steps): x = self.start_x + (event.x - self.start_x) * i / steps y = self.start_y + (event.y - self.start_y) * i / steps items = current_canvas.find_overlapping( x - half_width, y - half_width, x + half_width, y + half_width ) # 只删除手动涂改的项目 for item in items: if item in all_drawing_items: current_canvas.delete(item) # 从drawing_items中移除 for drawing in self.drawing_items: if item in drawing: drawing.remove(item) self.start_x = event.x self.start_y = event.y def get_all_drawing_items(self): """获取所有绘制项目的ID""" all_items = [] for drawing in self.drawing_items: all_items.extend(drawing) return all_items def stop_drawing(self, event): """停止手动涂改或擦除""" if self.current_drawing and (self.drawing_mode or self.erasing_mode): if self.drawing_mode: # 只在涂改模式下保存绘制项目 self.drawing_items.append(self.current_drawing.copy()) self.start_x = None self.start_y = None self.current_drawing = [] def undo_drawing(self): """撤回最后一次涂改""" if not self.drawing_items: return # 获取最后一次涂改的项目 last_drawing = self.drawing_items.pop() # 从两个Canvas中删除这些项目 for item_id in last_drawing: self.canvas1.delete(item_id) self.canvas2.delete(item_id) self.status_bar.config(text="已撤回最后一次涂改") def browse_file(self): try: file_path = filedialog.askopenfilename(filetypes=[("PDF文件", "*.pdf")]) if file_path: self.file_path.delete(0, tk.END) self.file_path.insert(0, file_path) self.pdf_path = file_path self.load_pdf() except Exception as e: messagebox.showerror("错误", f"浏览文件时发生错误: {str(e)}") def load_pdf(self): if not self.pdf_path: return try: if self.pdf_document: self.pdf_document.close() self.pdf_document = fitz.open(self.pdf_path) self.total_pages = len(self.pdf_document) self.current_page = 0 self.display_page() self.update_scrollbar() except Exception as e: messagebox.showerror("错误", f"无法打开PDF文件: {str(e)}") def display_page(self): if not self.pdf_document or self.total_pages == 0: return self.page_label.config(text=f"页面: {self.current_page+1}-{min(self.current_page+2, self.total_pages)}/{self.total_pages}") try: # 清空当前图像引用 self.current_images = [] # 显示第一页 if self.current_page < self.total_pages: page1 = self.pdf_document[self.current_page] zoom = 1.0 # 缩小一点以适应两页显示 mat = fitz.Matrix(zoom, zoom) pix1 = page1.get_pixmap(matrix=mat) # 转换为PhotoImage img_data1 = pix1.tobytes("ppm") img1 = tk.PhotoImage(data=img_data1) self.current_images.append(img1) # 保持引用 # 更新Canvas self.canvas1.config(width=min(img1.width(), 550), height=min(img1.height(), 500)) self.canvas1.delete("all") self.canvas1.create_image(0, 0, anchor=tk.NW, image=img1) # 设置滚动区域 self.canvas1.config(scrollregion=self.canvas1.bbox(tk.ALL)) # 显示第二页(如果有) if self.current_page + 1 < self.total_pages: page2 = self.pdf_document[self.current_page + 1] zoom = 1.0 # 缩小一点以适应两页显示 mat = fitz.Matrix(zoom, zoom) pix2 = page2.get_pixmap(matrix=mat) # 转换为PhotoImage img_data2 = pix2.tobytes("ppm") img2 = tk.PhotoImage(data=img_data2) self.current_images.append(img2) # 保持引用 # 更新Canvas self.canvas2.config(width=min(img2.width(), 550), height=min(img2.height(), 500)) self.canvas2.delete("all") self.canvas2.create_image(0, 0, anchor=tk.NW, image=img2) # 设置滚动区域 self.canvas2.config(scrollregion=self.canvas2.bbox(tk.ALL)) else: # 如果没有第二页,清空第二个Canvas self.canvas2.delete("all") self.canvas2.config(width=0, height=0) # 更新滚动条位置 self.update_scrollbar() except Exception as e: messagebox.showerror("错误", f"显示页面时发生错误: {str(e)}") def prev_page(self): if self.current_page > 0: self.current_page = max(0, self.current_page - 2) self.display_page() def next_page(self): if self.pdf_document and self.current_page + 2 < self.total_pages: self.current_page += 2 self.display_page() def process_pdf(self): if not self.pdf_path: messagebox.showwarning("警告", "请先选择PDF文件") return self.status_bar.config(text="正在处理PDF...") self.root.update() try: # 创建临时文件保存处理后的PDF temp_file = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) self.highlighted_pdf_path = temp_file.name temp_file.close() # 打开原始PDF doc = fitz.open(self.pdf_path) # 遍历每一页 for page_num in range(len(doc)): page = doc[page_num] # 获取页面文本 - 使用更精确的搜索方法 text = page.get_text("text") # 查找所有日期格式的文本 date_pattern = r"\d{4}-\d{2}-\d{2}" dates = re.findall(date_pattern, text) print(f"在第 {page_num+1} 页找到的日期: {dates}") # 调试信息 # 对于每个找到的日期,查找其在页面上的位置并高亮 for date_str in dates: highlight_type = self.should_highlight(date_str) if highlight_type > 0: print(f"高亮日期: {date_str}, 类型: {highlight_type}") # 调试信息 # 搜索日期文本的位置 text_instances = page.search_for(date_str) print(f"找到 {len(text_instances)} 个匹配项") # 调试信息 for inst in text_instances: # 扩大高亮区域 - 扩大2倍 expanded_rect = self.expand_rect(inst, 2.0) # 使用高亮注释 highlight = page.add_highlight_annot(expanded_rect) # 根据日期类型设置颜色 if highlight_type == 1: # 周五 highlight.set_colors(stroke=self.friday_color) elif highlight_type == 2: # 周日 highlight.set_colors(stroke=self.sunday_color) elif highlight_type == 3: # 指定红日 highlight.set_colors(stroke=self.other_red_color) highlight.set_opacity(self.highlight_alpha/100) # 修正透明度设置 highlight.update() # 保存处理后的PDF doc.save(self.highlighted_pdf_path) doc.close() # 重新加载处理后的PDF if self.pdf_document: self.pdf_document.close() self.pdf_document = fitz.open(self.highlighted_pdf_path) self.total_pages = len(self.pdf_document) self.current_page = 0 self.display_page() self.status_bar.config(text="处理完成") messagebox.showinfo("成功", "PDF处理完成,日期已高亮显示") except Exception as e: self.status_bar.config(text="处理失败") error_msg = f"处理PDF时发生错误: {str(e)}\n\n详细信息:\n{traceback.format_exc()}" messagebox.showerror("错误", error_msg) def expand_rect(self, rect, factor): """扩大矩形区域""" width = rect.width height = rect.height # 计算扩大后的尺寸 new_width = width * factor new_height = height * factor # 计算中心点 center_x = rect.x0 + width / 2 center_y = rect.y0 + height / 2 # 创建新矩形 new_rect = fitz.Rect( center_x - new_width / 2, center_y - new_height / 2, center_x + new_width / 2, center_y + new_height / 2 ) return new_rect def should_highlight(self, date_str): try: date_obj = datetime.strptime(date_str, "%Y-%m-%d") weekday = date_obj.weekday() # 检查是否为指定日期 is_specified_date = date_str in self.specified_dates # 检查是否为周五(4)或周日(6) if weekday == 4: # 周五 # 如果同时是指定日期,则使用指定红日颜色 if is_specified_date: return 3 # 指定红日 else: return 1 # 周五 elif weekday == 6: # 周日 return 2 # 周日 elif is_specified_date: # 指定指定日期 return 3 # 指定红日 except ValueError: pass return 0 # 不高亮 def show_print_dialog(self): """显示打印设置对话框""" if not self.highlighted_pdf_path or not os.path.exists(self.highlighted_pdf_path): messagebox.showwarning("警告", "请先处理PDF文件") return try: # 创建打印设置对话框 print_dialog = tk.Toplevel(self.root) print_dialog.title("打印设置") print_dialog.geometry("500x600") print_dialog.transient(self.root) print_dialog.grab_set() # 主框架 - 使用grid布局 main_frame = ttk.Frame(print_dialog, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 打印机信息区域 printer_frame = ttk.LabelFrame(main_frame, text="打印机", padding="5") printer_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) printer_frame.columnconfigure(1, weight=1) # 获取系统打印机列表 printers = self.get_printer_list() # 打印机名称和属性 ttk.Label(printer_frame, text="名称(M):").grid(row=0, column=0, sticky=tk.W, pady=2) # 打印机选择下拉框 self.printer_var = tk.StringVar() if printers: self.printer_var.set(printers[0]) # 默认选择第一个打印机 printer_combo = ttk.Combobox(printer_frame, textvariable=self.printer_var, values=printers, state="readonly") printer_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=2) ttk.Button(printer_frame, text="属性(P)...", command=self.show_printer_properties).grid(row=0, column=2, padx=5, pady=2) # 打印选项区域 options_frame = ttk.LabelFrame(main_frame, text="打印选项", padding="5") options_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5) options_frame.columnconfigure(1, weight=1) # 纸张来源 ttk.Label(options_frame, text="纸张来源(S):").grid(row=0, column=0, sticky=tk.W, pady=2) paper_combo = ttk.ComboBox(options_frame, values=["使用打印机设置", "自动选择", "手动送纸"], width=20, state="readonly") paper_combo.set("使用打印机设置") paper_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2) # 选择纸张 ttk.Label(options_frame, text="选择纸张:").grid(row=1, column=0, sticky=tk.W, pady=5) paper_type_combo = ttk.Combobox(options_frame, values=["A4", "Letter", "Legal", "A3"], width=20, state="readonly") paper_type_combo.set("A4") paper_type_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5) # 打印范围区域 range_frame = ttk.LabelFrame(main_frame, text="页码范围", padding="5") range_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5) range_frame.columnconfigure(0, weight=1) # 页码范围选择 self.range_var = tk.StringVar(value="全部") ttk.Radiobutton(range_frame, text="全部(A)", variable=self.range_var, value="全部").grid(row=0, column=0, sticky=tk.W) ttk.Radiobutton(range_frame, text="当前页(U)", variable=self.range_var, value="当前页").grid(row=1, column=0, sticky=tk.W) ttk.Radiobutton(range_frame, text="页码范围(G)", variable=self.range_var, value="页码范围").grid(row=2, column=0, sticky=tk.W) # 页码范围输入 self.range_entry = ttk.Entry(range_frame) self.range_entry.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=2) ttk.Label(range_frame, text="请键入页码和/或用逗号分隔的页码范围 (例如:1,3,5-12)").grid(row=4, column=0, sticky=tk.W) # 副本区域 copies_frame = ttk.LabelFrame(main_frame, text="副本", padding="5") copies_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5) # 份数 self.copies_var = tk.IntVar(value=1) copies_spin = ttk.Spinbox(copies_frame, from_=1, to=999, textvariable=self.copies_var, width=5) copies_spin.grid(row=0, column=0, sticky=tk.W, padx=5) ttk.Label(copies_frame, text="份数(C)").grid(row=0, column=1, sticky=tk.W) # 逐份打印 self.collate_var = tk.BooleanVar(value=True) ttk.Checkbutton(copies_frame, text="逐份打印(T)", variable=self.collate_var).grid(row=0, column=2, sticky=tk.W, padx=20) # 并打和缩放区域 layout_frame = ttk.LabelFrame(main_frame, text="并打和缩放", padding="5") layout_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=5) layout_frame.columnconfigure(1, weight=1) # 每页版数 ttk.Label(layout_frame, text="每页的版数(H):").grid(row=0, column=0, sticky=tk.W, pady=2) self.pages_combo = ttk.Combobox(layout_frame, values=["1", "2", "4", "6", "9", "16"], width=5, state="readonly") self.pages_combo.set("1") self.pages_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2) # 按纸型缩放 ttk.Label(layout_frame, text="按纸型缩放(Z):").grid(row=1, column=0, sticky=tk.W, pady=2) self.scale_combo = ttk.Combobox(layout_frame, values=["无缩放", "缩小至可打印区域", "适合可打印区域", "实际大小"], width=20, state="readonly") self.scale_combo.set("无缩放") self.scale_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2) # 并打顺序 ttk.Label(layout_frame, text="并打顺序:").grid(row=2, column=0, sticky=tk.W, pady=2) self.order_var = tk.StringVar(value="从左到右") ttk.Radiobutton(layout_frame, text="从左到右(F)", variable=self.order_var, value="从左到右").grid(row=2, column=1, sticky=tk.W) ttk.Radiobutton(layout_frame, text="从上到下(B)", variable=self.order_var, value="从上到下").grid(row=2, column=2, sticky=tk.W, padx=5) # 分隔线 self.separator_var = tk.BooleanVar(value=False) ttk.Checkbutton(layout_frame, text="并打时绘制分隔线(D)", variable=self.separator_var).grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=2) # 其他选项 other_frame = ttk.Frame(layout_frame) other_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=2) self.reverse_var = tk.BooleanVar(value=False) ttk.Checkbutton(other_frame, text="反片打印(I)", variable=self.reverse_var).grid(row=0, column=0, sticky=tk.W) self.to_file_var = tk.BooleanVar(value=False) ttk.Checkbutton(other_frame, text="打印到文件(L)", variable=self.to_file_var).grid(row=0, column=1, sticky=tk.W, padx=5) self.duplex_var = tk.BooleanVar(value=False) ttk.Checkbutton(other_frame, text="双面打印(X)", variable=self.duplex_var).grid(row=0, column=2, sticky=tk.W, padx=5) # 按钮区域 button_frame = ttk.Frame(main_frame) button_frame.grid(row=5, column=0, sticky=tk.E, pady=10) ttk.Button(button_frame, text="打印预览", command=self.print_preview).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="取消", command=print_dialog.destroy).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="开始打印", command=lambda: self.print_pdf(print_dialog)).pack(side=tk.LEFT, padx=5) # 配置权重使组件可以扩展 main_frame.columnconfigure(0, weight=1) for i in range(6): main_frame.rowconfigure(i, weight=0) main_frame.rowconfigure(5, weight=1) except Exception as e: messagebox.showerror("错误", f"打开打印设置时发生错误: {str(e)}") def get_printer_list(self): """获取系统打印机列表""" if not WIN32PRINT_AVAILABLE: # 如果win32print不可用,尝试使用系统命令获取打印机列表 try: if sys.platform == "win32": # Windows系统 result = subprocess.run(['wmic', 'printer', 'get', 'name'], capture_output=True, text=True) if result.returncode == 0: printers = [] lines = result.stdout.split('\n') for line in lines: line = line.strip() if line and not line.startswith('Name'): printers.append(line) return printers if printers else ["默认打印机"] elif sys.platform == "darwin": # macOS系统 result = subprocess.run(['lpstat', '-a'], capture_output=True, text=True) if result.returncode == 0: printers = [] lines = result.stdout.split('\n') for line in lines: if line: parts = line.split() if parts: printers.append(parts[0]) return printers if printers else ["默认打印机"] else: # Linux系统 result = subprocess.run(['lpstat', '-a'], capture_output=True, text=True) if result.returncode == 0: printers = [] lines = result.stdout.split('\n') for line in lines: if line: parts = line.split() if parts: printers.append(parts[0]) return printers if printers else ["默认打印机"] except: pass return ["默认打印机"] try: printers = [] for printer in win32print.EnumPrinters(win32print.PRENER_ENUM_LOCAL | win32print.PRENER_ENUM_CONNECTIONS): printers.append(printer[2]) return printers except: # 如果无法获取打印机列表,返回默认打印机 try: default_printer = win32print.GetDefaultPrinter() return [default_printer] if default_printer else ["默认打印机"] except: return ["默认打印机"] def show_printer_properties(self): """显示打印机属性""" if not WIN32PRINT_AVAILABLE: messagebox.showinfo("打印机属性", "在此系统上无法访问打印机属性。") return try: printer_name = self.printer_var.get() if printer_name and printer_name != "默认打印机": # 打开打印机属性对话框 hprinter = win32print.OpenPrinter(printer_name) win32print.PrinterProperties(self.root.winfo_id(), hprinter) win32print.ClosePrinter(hprinter) else: messagebox.showinfo("打印机属性", "请先选择一个有效的打印机。") except Exception as e: messagebox.showerror("错误", f"无法打开打印机属性: {str(e)}") def print_preview(self): """打印预览功能""" if not self.highlighted_pdf_path or not os.path.exists(self.highlighted_pdf_path): messagebox.showwarning("警告", "请先处理PDF文件") return try: # 创建预览窗口 preview = tk.Toplevel(self.root) preview.title("打印预览") preview.geometry("800x600") # 创建Canvas用于显示预览 preview_canvas = tk.Canvas(preview, bg="white") preview_canvas.pack(fill=tk.BOTH, expand=True) # 添加滚动条 v_scrollbar = ttk.Scrollbar(preview, orient=tk.VERTICAL, command=preview_canvas.yview) h_scrollbar = ttk.Scrollbar(preview, orient=tk.HORIZONTAL, command=preview_canvas.xview) preview_canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) preview_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 加载PDF第一页作为预览 doc = fitz.open(self.highlighted_pdf_path) page = doc[0] zoom = 1.0 mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat) # 转换为PhotoImage img_data = pix.tobytes("ppm") img = tk.PhotoImage(data=img_data) # 保持引用 preview.img_ref = img # 显示图像 preview_canvas.create_image(0, 0, anchor=tk.NW, image=img) preview_canvas.config(scrollregion=preview_canvas.bbox(tk.ALL)) doc.close() except Exception as e: messagebox.showerror("错误", f"打印预览时发生错误: {str(e)}") def print_pdf(self, dialog): """实际打印PDF""" dialog.destroy() if not self.highlighted_pdf_path or not os.path.exists(self.highlighted_pdf_path): messagebox.showwarning("警告", "请先处理PDF文件") return try: # 获取打印设置 printer_name = self.printer_var.get() range_type = self.range_var.get() page_range = self.range_entry.get() if range_type == "页码范围" else None copies = self.copies_var.get() # 构建打印命令 if sys.platform == "win32": # 使用系统打印对话框 os.startfile(self.highlighted_pdf_path, "print") elif sys.platform == "darwin": # macOS cmd = ['lp'] if printer_name and printer_name != "默认打印机": cmd.extend(['-d', printer_name]) if copies > 1: cmd.extend(['-n', str(copies)]) if page_range: cmd.extend(['-o', f'page-ranges={page_range}']) cmd.append(self.highlighted_pdf_path) subprocess.run(cmd) else: # Linux cmd = ['lp'] if printer_name and printer_name != "默认打印机": cmd.extend(['-d', printer_name]) if copies > 1: cmd.extend(['-n', str(copies)]) if page_range: cmd.extend(['-o', f'page-ranges={page_range}']) cmd.append(self.highlighted_pdf_path) subprocess.run(cmd) self.status_bar.config(text="打印任务已发送") except Exception as e: messagebox.showerror("错误", f"打印失败: {str(e)}") def __del__(self): # 清理临时文件 if self.highlighted_pdf_path and os.path.exists(self.highlighted_pdf_path): try: os.unlink(self.highlighted_pdf_path) except: pass # 关闭PDF文档 if self.pdf_document: try: self.pdf_document.close() except: pass def main(): try: # 检查是否安装了PyMuPDF try: import fitz except ImportError: messagebox.showerror("缺少依赖", "请安装PyMuPDF库: pip install PyMuPDF") return root = tk.Tk() app = PDFDateHighlighter(root) root.mainloop() except Exception as e: error_msg = f"程序发生未预期错误: {str(e)}\n\n详细信息:\n{traceback.format_exc()}" messagebox.showerror("严重错误", error_msg) if __name__ == "__main__": main() 已安装1. Tesseract OCR,2. pdf2image库,3.Poppler工具,为何点击“PDF转COR”按钮后报错 错误 OCR功能不可用,请确保已安装: 1. Tesseract OCR 2. pdf2image库 3.Poppler工具 修改一套完整代码
最新发布
08-22
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值