import sysimport jsonimport osfrom pathlib import Pathimport subprocessfrom PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QTextEdit, QProgressBar, QGroupBox, QMessageBox, QLineEdit, QGridLayout, QTabWidget, QCheckBox, QSpinBox, QDoubleSpinBox)from PyQt6.QtCore import Qt, QThread, pyqtSignalfrom PyQt6.QtGui import QTextCursor# --- 配置文件路径 ---CONFIG_FILE = "api_config.json"# --- 配置管理函数 ---def load_config(): """加载配置文件""" default_config = { "qwen_api_key": "", "paddle_api_url": "", "paddle_token": "", "output_dir": "", "last_folder": "", "red_threshold": 0.1, "extract_seal_only": True } if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, 'r', encoding='utf-8') as f: config = json.load(f) # 确保所有必要的键都存在 for key in default_config: if key not in config: config[key] = default_config[key] return config except Exception as e: print(f"读取配置文件失败: {str(e)},使用默认配置") return default_config else: return default_configdef save_config(config): """保存配置文件""" try: with open(CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) return True except Exception as e: print(f"保存配置文件失败: {str(e)}") return False# --- 抽象工作线程基类 ---class BaseWorker(QThread): """工作线程基类""" progress_signal = pyqtSignal(str) progress_value_signal = pyqtSignal(int) result_signal = pyqtSignal(dict) error_signal = pyqtSignal(str) def __init__(self): super().__init__() self.running = True def stop(self): """停止线程""" self.running = Falseclass QwenImageWorker(BaseWorker): """Qwen Image Edit 工作线程 - 接口定义""" def __init__(self, image_paths, api_key, prompt_text): super().__init__() self.image_paths = image_paths self.api_key = api_key self.prompt_text = prompt_text def run(self): """线程执行函数""" total_images = len(self.image_paths) for idx, image_path in enumerate(self.image_paths): if not self.running: break # 模拟处理过程 self.progress_value_signal.emit(int((idx / total_images) * 100)) self.progress_signal.emit(f"正在处理第 {idx + 1}/{total_images} 张图片: {os.path.basename(image_path)}") # 模拟API调用延时 self.msleep(500) # 模拟结果 self.result_signal.emit({ 'success': True, 'original_file': image_path, 'processed_files': [f"output/{os.path.basename(image_path)}"], 'message': f"成功处理: {os.path.basename(image_path)}" }) self.progress_value_signal.emit(100) self.progress_signal.emit("处理完成!")class PaddleOCRWorker(BaseWorker): """PaddleOCR-VL-1.5 工作线程 - 接口定义""" def __init__(self, file_paths, api_url, token, input_base_dir, output_base_dir, extract_seal_only=True, red_threshold=0.1): super().__init__() self.file_paths = file_paths self.api_url = api_url self.token = token self.input_base_dir = input_base_dir self.output_base_dir = output_base_dir self.extract_seal_only = extract_seal_only self.red_threshold = red_threshold def run(self): """线程执行函数 - 需要在实际应用中实现""" total_files = len(self.file_paths) for idx, file_path in enumerate(self.file_paths): if not self.running: break # 模拟处理过程 self.progress_value_signal.emit(int((idx / total_files) * 100)) self.progress_signal.emit(f"处理文件 {idx + 1}/{total_files}: {os.path.basename(file_path)}") # 模拟处理延时 self.msleep(300) # 模拟结果 self.result_signal.emit({ 'success': True, 'file': file_path, 'message': f"成功处理: {os.path.basename(file_path)}" }) self.progress_value_signal.emit(100) self.progress_signal.emit(f"批量处理完成!成功: {total_files}, 失败: 0")# --- 主界面类 ---class ImageProcessingApp(QMainWindow): """图片处理应用主窗口 - 界面框架""" def __init__(self): super().__init__() self.worker = None # 加载配置 self.config = load_config() self.qwen_api_key = self.config.get("qwen_api_key", "") self.paddle_api_url = self.config.get("paddle_api_url", "") self.paddle_token = self.config.get("paddle_token", "") self.init_ui() def init_ui(self): """初始化UI""" self.setWindowTitle("图片处理工具 - 界面框架 (欢迎关注微信公众号:码海听潮)") self.setGeometry(100, 100, 700, 500) # 设置样式 self.setStyleSheet(""" QMainWindow { background-color: #f0f2f5; } QTabWidget::pane { border: 1px solid #d1d9e6; border-radius: 8px; background-color: white; } QTabBar::tab { font-size: 13px; font-weight: bold; padding: 8px 16px; margin-right: 2px; background-color: #e6e9f0; border: 1px solid #d1d9e6; border-bottom: none; border-top-left-radius: 6px; border-top-right-radius: 6px; } QTabBar::tab:selected { background-color: white; border-bottom: 2px solid #3498db; } QGroupBox { font-size: 13px; font-weight: bold; border: 1px solid #d1d9e6; border-radius: 6px; margin-top: 8px; padding-top: 8px; background-color: white; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: #2c3e50; } QPushButton { font-size: 13px; padding: 8px 16px; border-radius: 5px; border: none; font-weight: bold; } QPushButton:hover { opacity: 0.9; } QPushButton:disabled { background-color: #bdc3c7; }#startButton { background-color: #2ecc71; color: white; font-size: 15px; padding: 12px 24px; }#folderButton { background-color: #3498db; color: white; padding: 8px 12px; }#saveConfigButton { background-color: #9b59b6; color: white; padding: 8px 12px; min-width: 100px; } QLabel { font-size: 13px; } QLineEdit { font-size: 13px; padding: 8px; border: 1px solid #d1d9e6; border-radius: 5px; background-color: white; } QTextEdit { border: 1px solid #d1d9e6; border-radius: 5px; padding: 6px; font-size: 12px; background-color: white; } QProgressBar { border: 1px solid #d1d9e6; border-radius: 5px; text-align: center; background-color: white; font-weight: bold; height: 20px; } QProgressBar::chunk { background-color: #3498db; border-radius: 5px; } """) # 创建中心部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(10) main_layout.setContentsMargins(10, 10, 10, 10) # 创建选项卡 self.tab_widget = QTabWidget() # 添加两个选项卡 self.tab_widget.addTab(self.create_paddle_tab(), "PaddleOCR 大模型公章提取") self.tab_widget.addTab(self.create_qwen_tab(), "Qwen-Image 大模型公章抠图") main_layout.addWidget(self.tab_widget) # 全局进度条 self.global_progress_bar = QProgressBar() self.global_progress_bar.setTextVisible(True) self.global_progress_bar.setFormat("%p%") self.global_progress_bar.setMaximumHeight(20) main_layout.addWidget(self.global_progress_bar) # 全局日志 log_group = QGroupBox("处理日志") log_group.setMaximumHeight(150) log_layout = QVBoxLayout() log_layout.setContentsMargins(8, 8, 8, 8) self.global_log_text = QTextEdit() self.global_log_text.setReadOnly(True) self.global_log_text.setMaximumHeight(120) log_layout.addWidget(self.global_log_text) log_group.setLayout(log_layout) main_layout.addWidget(log_group) # 底部按钮 bottom_layout = QHBoxLayout() bottom_layout.addStretch() self.save_config_button = QPushButton("💾 保存配置") self.save_config_button.setObjectName("saveConfigButton") self.save_config_button.clicked.connect(self.save_all_config) self.clear_log_button = QPushButton("🗑️ 清空日志") self.clear_log_button.setObjectName("saveConfigButton") self.clear_log_button.clicked.connect(self.clear_log) bottom_layout.addWidget(self.save_config_button) bottom_layout.addWidget(self.clear_log_button) bottom_layout.addStretch() main_layout.addLayout(bottom_layout) def create_paddle_tab(self): """创建PaddleOCR-VL-1.5选项卡""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) # API设置组 api_group = QGroupBox("API设置") api_layout = QGridLayout() api_layout.setSpacing(8) api_layout.setContentsMargins(8, 8, 8, 8) # API URL api_url_label = QLabel("API URL:") self.paddle_api_url_edit = QLineEdit() self.paddle_api_url_edit.setText(self.paddle_api_url) self.paddle_api_url_edit.setPlaceholderText("请输入PaddleOCR API URL") # Token token_label = QLabel("Token:") self.paddle_token_edit = QLineEdit() self.paddle_token_edit.setText(self.paddle_token) self.paddle_token_edit.setPlaceholderText("请输入Token") # 红色检测阈值 threshold_label = QLabel("红色阈值:") self.red_threshold_spin = QDoubleSpinBox() self.red_threshold_spin.setRange(0.01, 0.5) self.red_threshold_spin.setSingleStep(0.01) self.red_threshold_spin.setValue(self.config.get("red_threshold", 0.1)) self.red_threshold_spin.setDecimals(2) self.red_threshold_spin.setSuffix(" (10%)") self.red_threshold_spin.setMaximumWidth(120) # 只提取公章选项 self.extract_seal_only_check = QCheckBox("只提取红色公章") self.extract_seal_only_check.setChecked(self.config.get("extract_seal_only", True)) api_layout.addWidget(api_url_label, 0, 0) api_layout.addWidget(self.paddle_api_url_edit, 0, 1, 1, 2) api_layout.addWidget(token_label, 1, 0) api_layout.addWidget(self.paddle_token_edit, 1, 1, 1, 2) api_layout.addWidget(threshold_label, 2, 0) api_layout.addWidget(self.red_threshold_spin, 2, 1) api_layout.addWidget(self.extract_seal_only_check, 2, 2) api_group.setLayout(api_layout) layout.addWidget(api_group) # 文件管理组 file_group = QGroupBox("文件管理") file_layout = QGridLayout() file_layout.setSpacing(8) file_layout.setContentsMargins(8, 8, 8, 8) # 文件夹路径输入框 folder_label = QLabel("文件夹:") self.paddle_folder_edit = QLineEdit() self.paddle_folder_edit.setPlaceholderText("选择或输入文件夹路径") self.paddle_folder_edit.setText(self.config.get("last_folder", "")) # 文件夹选择按钮 self.paddle_folder_button = QPushButton("浏览") self.paddle_folder_button.setObjectName("folderButton") self.paddle_folder_button.clicked.connect(self.select_paddle_folder) self.paddle_folder_button.setMaximumWidth(60) # 输出目录 output_label = QLabel("输出:") self.paddle_output_edit = QLineEdit() self.paddle_output_edit.setText(self.config.get("output_dir", "paddle_output")) self.paddle_output_edit.setPlaceholderText("输出目录") # 输出目录浏览按钮 self.paddle_output_button = QPushButton("浏览") self.paddle_output_button.setObjectName("folderButton") self.paddle_output_button.clicked.connect(self.select_paddle_output_folder) self.paddle_output_button.setMaximumWidth(60) file_layout.addWidget(folder_label, 0, 0) file_layout.addWidget(self.paddle_folder_edit, 0, 1) file_layout.addWidget(self.paddle_folder_button, 0, 2) file_layout.addWidget(output_label, 1, 0) file_layout.addWidget(self.paddle_output_edit, 1, 1) file_layout.addWidget(self.paddle_output_button, 1, 2) file_group.setLayout(file_layout) layout.addWidget(file_group) # 开始按钮 self.paddle_start_button = QPushButton("🚀 开始公章提取") self.paddle_start_button.setObjectName("startButton") self.paddle_start_button.clicked.connect(self.start_paddle_processing) layout.addWidget(self.paddle_start_button) layout.addStretch() return tab def create_qwen_tab(self): """创建Qwen-Image选项卡""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) # API设置组 api_group = QGroupBox("API设置") api_layout = QHBoxLayout() api_layout.setSpacing(8) api_layout.setContentsMargins(8, 8, 8, 8) api_key_label = QLabel("API Key:") self.qwen_api_key_edit = QLineEdit() self.qwen_api_key_edit.setText(self.qwen_api_key) self.qwen_api_key_edit.setPlaceholderText("请输入Qwen-Image API Key") api_layout.addWidget(api_key_label) api_layout.addWidget(self.qwen_api_key_edit) api_group.setLayout(api_layout) layout.addWidget(api_group) # 图片管理组 image_group = QGroupBox("图片管理") image_layout = QGridLayout() image_layout.setSpacing(8) image_layout.setContentsMargins(8, 8, 8, 8) # 文件夹路径输入框 folder_label = QLabel("文件夹:") self.qwen_folder_edit = QLineEdit() self.qwen_folder_edit.setPlaceholderText("选择或输入图片文件夹路径") self.qwen_folder_edit.setText(self.config.get("last_folder", "")) # 文件夹选择按钮 self.qwen_folder_button = QPushButton("浏览") self.qwen_folder_button.setObjectName("folderButton") self.qwen_folder_button.clicked.connect(self.select_qwen_folder) self.qwen_folder_button.setMaximumWidth(60) # 输出目录 output_label = QLabel("输出:") self.qwen_output_edit = QLineEdit() self.qwen_output_edit.setText(self.config.get("output_dir", "qwen_output")) self.qwen_output_edit.setPlaceholderText("输出目录") # 输出目录浏览按钮 self.qwen_output_button = QPushButton("浏览") self.qwen_output_button.setObjectName("folderButton") self.qwen_output_button.clicked.connect(self.select_qwen_output_folder) self.qwen_output_button.setMaximumWidth(60) image_layout.addWidget(folder_label, 0, 0) image_layout.addWidget(self.qwen_folder_edit, 0, 1) image_layout.addWidget(self.qwen_folder_button, 0, 2) image_layout.addWidget(output_label, 1, 0) image_layout.addWidget(self.qwen_output_edit, 1, 1) image_layout.addWidget(self.qwen_output_button, 1, 2) image_group.setLayout(image_layout) layout.addWidget(image_group) # 提示词设置组 prompt_group = QGroupBox("提示词") prompt_layout = QVBoxLayout() prompt_layout.setSpacing(5) prompt_layout.setContentsMargins(8, 8, 8, 8) self.prompt_edit = QTextEdit() default_prompt = "请仔细识别图片中的公章,确保:\n1. 对公章进行抠图\n2. 保持公章在原图中的实际尺寸\n3. 去除公章下方或周围的黑色文字,使公章背景干净透明\n4. 保持公章颜色与原图完全一致\n5. 保留公章的所有红色字体" self.prompt_edit.setText(default_prompt) self.prompt_edit.setMinimumHeight(100) self.prompt_edit.setMaximumHeight(120) prompt_layout.addWidget(self.prompt_edit) prompt_group.setLayout(prompt_layout) layout.addWidget(prompt_group) # 开始按钮 self.qwen_start_button = QPushButton("🚀 开始公章抠图") self.qwen_start_button.setObjectName("startButton") self.qwen_start_button.clicked.connect(self.start_qwen_processing) layout.addWidget(self.qwen_start_button) layout.addStretch() return tab # ==================== 文件夹选择函数 ==================== def select_paddle_output_folder(self): """选择PaddleOCR输出目录""" current_path = self.paddle_output_edit.text().strip() if not os.path.exists(current_path): current_path = os.path.dirname(self.paddle_folder_edit.text().strip()) if self.paddle_folder_edit.text().strip() else "" folder = QFileDialog.getExistingDirectory( self, "选择输出目录", current_path ) if folder: self.paddle_output_edit.setText(folder) self.config["output_dir"] = folder def select_qwen_output_folder(self): """选择Qwen输出目录""" current_path = self.qwen_output_edit.text().strip() if not os.path.exists(current_path): current_path = os.path.dirname(self.qwen_folder_edit.text().strip()) if self.qwen_folder_edit.text().strip() else "" folder = QFileDialog.getExistingDirectory( self, "选择输出目录", current_path ) if folder: self.qwen_output_edit.setText(folder) self.config["output_dir"] = folder def select_paddle_folder(self): """选择PaddleOCR处理文件夹""" current_path = self.paddle_folder_edit.text().strip() if not os.path.exists(current_path): current_path = self.config.get("last_folder", "") folder = QFileDialog.getExistingDirectory( self, "选择文件夹 (包含PDF或图片)", current_path ) if folder: self.paddle_folder_edit.setText(folder) self.config["last_folder"] = folder def select_qwen_folder(self): """选择Qwen处理文件夹""" current_path = self.qwen_folder_edit.text().strip() if not os.path.exists(current_path): current_path = self.config.get("last_folder", "") folder = QFileDialog.getExistingDirectory( self, "选择图片文件夹", current_path ) if folder: self.qwen_folder_edit.setText(folder) self.config["last_folder"] = folder # ==================== 处理函数 ==================== def start_paddle_processing(self): """开始PaddleOCR处理 - 接口方法,需要在实际应用中重写""" folder = self.paddle_folder_edit.text().strip() if not folder or not os.path.exists(folder): QMessageBox.warning(self, "警告", "请选择有效的文件夹!") return api_url = self.paddle_api_url_edit.text().strip() if not api_url: QMessageBox.warning(self, "警告", "请输入API URL!") return token = self.paddle_token_edit.text().strip() if not token: QMessageBox.warning(self, "警告", "请输入Token!") return output_dir = self.paddle_output_edit.text().strip() if not output_dir: output_dir = "paddle_output" os.makedirs(output_dir, exist_ok=True) # 收集文件 extensions = ['.pdf', '.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif'] file_paths = [] for ext in extensions: file_paths.extend([str(p) for p in Path(folder).glob(f'**/*{ext}')]) if not file_paths: QMessageBox.warning(self, "警告", "文件夹中没有找到支持的PDF或图片文件!") return # 禁用按钮 self.paddle_start_button.setEnabled(False) self.save_config_button.setEnabled(False) # 重置进度条 self.global_progress_bar.setMaximum(100) self.global_progress_bar.setValue(0) # 获取设置 extract_seal_only = self.extract_seal_only_check.isChecked() red_threshold = self.red_threshold_spin.value() # 创建并启动工作线程 self.worker = PaddleOCRWorker( file_paths, api_url, token, folder, output_dir, extract_seal_only, red_threshold ) # 连接信号 self.worker.progress_signal.connect(self.log_message) self.worker.progress_value_signal.connect(self.global_progress_bar.setValue) self.worker.result_signal.connect(self.on_paddle_result) self.worker.error_signal.connect(self.log_message) self.worker.finished.connect(self.on_worker_finished) # 开始处理 self.log_message(f"开始PaddleOCR处理,共 {len(file_paths)} 个文件...") self.log_message(f"提取公章模式: {'启用'if extract_seal_only else'禁用'}") self.log_message(f"红色检测阈值: {red_threshold}") self.worker.start() def start_qwen_processing(self): """开始Qwen处理 - 接口方法,需要在实际应用中重写""" folder = self.qwen_folder_edit.text().strip() if not folder or not os.path.exists(folder): QMessageBox.warning(self, "警告", "请选择有效的图片文件夹!") return api_key = self.qwen_api_key_edit.text().strip() if not api_key: QMessageBox.warning(self, "警告", "请输入API Key!") return prompt = self.prompt_edit.toPlainText().strip() if not prompt: QMessageBox.warning(self, "警告", "请输入提示词!") return output_dir = self.qwen_output_edit.text().strip() if not output_dir: output_dir = "qwen_output" os.makedirs(output_dir, exist_ok=True) self.config["output_dir"] = output_dir # 收集图片 extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif'] image_paths = [] for ext in extensions: image_paths.extend([str(p) for p in Path(folder).glob(f'**/*{ext}')]) image_paths.extend([str(p) for p in Path(folder).glob(f'**/*{ext.upper()}')]) if not image_paths: QMessageBox.warning(self, "警告", "文件夹中没有找到支持的图片文件!") return # 禁用按钮 self.qwen_start_button.setEnabled(False) self.save_config_button.setEnabled(False) # 重置进度条 self.global_progress_bar.setMaximum(100) self.global_progress_bar.setValue(0) # 创建并启动工作线程 self.worker = QwenImageWorker( image_paths, api_key, prompt ) # 连接信号 self.worker.progress_signal.connect(self.log_message) self.worker.progress_value_signal.connect(self.global_progress_bar.setValue) self.worker.result_signal.connect(self.on_qwen_result) self.worker.error_signal.connect(self.log_message) self.worker.finished.connect(self.on_worker_finished) # 开始处理 self.log_message(f"开始Qwen处理,共 {len(image_paths)} 张图片...") self.worker.start() def on_paddle_result(self, result): """处理PaddleOCR结果""" if result['success']: self.log_message(f"✓ {result['message']}") else: self.log_message(f"✗ {result['message']}") def on_qwen_result(self, result): """处理Qwen结果""" if result['success']: self.log_message(f"✓ {result['message']}") else: self.log_message(f"✗ {result['message']}") def on_worker_finished(self): """工作线程完成""" self.paddle_start_button.setEnabled(True) self.qwen_start_button.setEnabled(True) self.save_config_button.setEnabled(True) self.log_message("所有处理完成!") # 询问是否打开结果文件夹 current_tab = self.tab_widget.currentIndex() if current_tab == 0: output_dir = self.paddle_output_edit.text().strip() else: output_dir = self.qwen_output_edit.text().strip() if os.path.exists(output_dir): reply = QMessageBox.question( self, "处理完成", "处理完成!是否打开结果文件夹?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: if sys.platform == "win32": os.startfile(output_dir) elif sys.platform == "darwin": subprocess.run(["open", output_dir]) else: subprocess.run(["xdg-open", output_dir]) except Exception as e: self.log_message(f"无法打开文件夹: {str(e)}") def log_message(self, message): """添加日志消息""" import time timestamp = time.strftime("%H:%M:%S", time.localtime()) self.global_log_text.append(f"[{timestamp}] {message}") cursor = self.global_log_text.textCursor() cursor.movePosition(QTextCursor.MoveOperation.End) self.global_log_text.setTextCursor(cursor) def clear_log(self): """清空日志""" self.global_log_text.clear() def save_all_config(self): """保存所有配置""" self.config["qwen_api_key"] = self.qwen_api_key_edit.text().strip() self.config["paddle_api_url"] = self.paddle_api_url_edit.text().strip() self.config["paddle_token"] = self.paddle_token_edit.text().strip() self.config["output_dir"] = self.paddle_output_edit.text().strip() or self.qwen_output_edit.text().strip() self.config["last_folder"] = self.paddle_folder_edit.text().strip() or self.qwen_folder_edit.text().strip() self.config["red_threshold"] = self.red_threshold_spin.value() self.config["extract_seal_only"] = self.extract_seal_only_check.isChecked() if save_config(self.config): QMessageBox.information(self, "成功", "所有配置已保存!") self.log_message("配置已保存到文件") else: QMessageBox.warning(self, "警告", "保存配置失败") def closeEvent(self, event): """关闭事件处理""" self.save_all_config() if self.worker and self.worker.isRunning(): reply = QMessageBox.question( self, "确认退出", "处理正在进行中,确定要退出吗?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self.worker.stop() self.worker.wait() event.accept() else: event.ignore() else: event.accept()# --- 主程序入口 ---def main(): app = QApplication(sys.argv) app.setStyle('Fusion') window = ImageProcessingApp() window.show() sys.exit(app.exec())if __name__ == "__main__": main()