From 6f59cd3c284634ce9dab3227fbabe639de8d9bef Mon Sep 17 00:00:00 2001 From: tsjykj <114121999@qq.com> Date: Tue, 1 Jul 2025 17:42:33 +0800 Subject: [PATCH] 20250701 --- .idea/.gitignore | 8 + .idea/deployment.xml | 14 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/pythonProject.iml | 10 + .idea/vcs.xml | 6 + analyzer.py | 121 +++ config/defaut_config.json | 21 + gui.py | 766 ++++++++++++++++++ main.py | 24 + utils.py | 63 ++ watermark_config.json | 1 + 13 files changed, 1055 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/deployment.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/pythonProject.iml create mode 100644 .idea/vcs.xml create mode 100644 analyzer.py create mode 100644 config/defaut_config.json create mode 100644 gui.py create mode 100644 main.py create mode 100644 utils.py create mode 100644 watermark_config.json diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..3029b8a --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..999f789 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e15ec35 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pythonProject.iml b/.idea/pythonProject.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/pythonProject.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/analyzer.py b/analyzer.py new file mode 100644 index 0000000..78c66b7 --- /dev/null +++ b/analyzer.py @@ -0,0 +1,121 @@ +import cv2 +import numpy as np +import json +from datetime import datetime + + +class WatermarkAnalyzer: + def __init__(self, config_path="config/default_config.json"): + self.watermark_regions = [] + self.selected_regions = [] + self.config_path = config_path + self.load_config() + self._check_opencv_version() + + def _check_opencv_version(self): + """检查OpenCV版本,确定是否支持显著性检测""" + self.has_saliency = hasattr(cv2, 'saliency') + if not self.has_saliency: + print("警告: 当前OpenCV版本不支持显著性检测,将使用替代方法") + + def load_config(self): + try: + with open(self.config_path, 'r') as f: + config = json.load(f) + self.inpaint_radius = config.get("inpaint_radius", 5) + self.inpaint_algorithm = config.get("inpaint_algorithm", cv2.INPAINT_TELEA) + self.dilate_size = config.get("dilate_size", 3) + self.use_adaptive_mask = config.get("use_adaptive_mask", True) + self.text_threshold = config.get("text_threshold", 180) + self.min_text_area = config.get("min_text_area", 100) + self.text_color_range = tuple( + tuple(c) for c in config.get("text_color_range", ((50, 50, 50), (180, 180, 180)))) + self.stamp_color_range = tuple( + tuple(c) for c in config.get("stamp_color_range", ((0, 0, 100), (100, 100, 255)))) + self.saliency_threshold = config.get("saliency_threshold", 0.5) + self.edge_threshold = config.get("edge_threshold", 100) + self.use_mser = config.get("use_mser", True) + self.use_color_filter = config.get("use_color_filter", True) + self.use_texture_analysis = config.get("use_texture_analysis", True) + except Exception as e: + print(f"加载配置失败: {e}") + self.reset_to_defaults() + + # ... 其他方法保持不变 ... + + def _detect_by_saliency(self, img): + """基于显著性检测水印 - 兼容旧版OpenCV""" + if self.has_saliency: + # 使用标准显著性检测(如果支持) + try: + saliency = cv2.saliency.StaticSaliencySpectralResidual_create() + _, saliency_map = saliency.computeSaliency(img) + + # 阈值处理 + _, saliency_mask = cv2.threshold(saliency_map * 255, self.saliency_threshold * 255, 255, + cv2.THRESH_BINARY) + saliency_mask = saliency_mask.astype(np.uint8) + + # 形态学操作 + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) + closed = cv2.morphologyEx(saliency_mask, cv2.MORPH_CLOSE, kernel) + + # 查找轮廓 + contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + regions = [] + for contour in contours: + x, y, w, h = cv2.boundingRect(contour) + area = w * h + if area > self.min_text_area * 2 and area < img.shape[0] * img.shape[1] * 0.1: + regions.append({ + 'type': 'saliency', + 'position': (x, y, w, h), + 'confidence': 0.7, + 'contour': contour + }) + + return regions + except Exception as e: + print(f"显著性检测失败: {e}") + self.has_saliency = False # 标记为不支持,下次使用替代方法 + + # 替代方法:使用亮度变化检测 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # 高斯模糊减少噪声 + blurred = cv2.GaussianBlur(gray, (15, 15), 0) + + # 计算拉普拉斯算子检测边缘 + laplacian = cv2.Laplacian(blurred, cv2.CV2.CV_64F) + laplacian = np.uint8(np.absolute(laplacian)) + + # 阈值处理 + _, threshold = cv2.threshold(laplacian, 30, 255, cv2.THRESH_BINARY) + + # 形态学操作 + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) + closed = cv2.morphologyEx(threshold, cv2.MORPH_CLOSE, kernel) + + # 查找轮廓 + contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + regions = [] + for contour in contours: + x, y, w, h = cv2.boundingRect(contour) + area = w * h + if area > self.min_text_area * 2 and area < img.shape[0] * img.shape[1] * 0.1: + # 计算轮廓的紧致度 + perimeter = cv2.arcLength(contour, True) + compactness = 4 * np.pi * area / (perimeter * perimeter) if perimeter > 0 else 0 + + # 水印通常具有中等紧致度 + if 0.1 < compactness < 0.9: + regions.append({ + 'type': 'contrast', + 'position': (x, y, w, h), + 'confidence': 0.6, + 'contour': contour + }) + + return regions \ No newline at end of file diff --git a/config/defaut_config.json b/config/defaut_config.json new file mode 100644 index 0000000..fed2456 --- /dev/null +++ b/config/defaut_config.json @@ -0,0 +1,21 @@ +{ + "inpaint_radius": 5, + "inpaint_algorithm": 0, + "dilate_size": 3, + "use_adaptive_mask": true, + "text_threshold": 180, + "min_text_area": 100, + "text_color_range": [ + [50, 50, 50], + [180, 180, 180] + ], + "stamp_color_range": [ + [0, 0, 100], + [100, 100, 255] + ], + "saliency_threshold": 0.5, + "edge_threshold": 100, + "use_mser": true, + "use_color_filter": true, + "use_texture_analysis": true +} \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..19731a9 --- /dev/null +++ b/gui.py @@ -0,0 +1,766 @@ +import os +import cv2 +import numpy as np +from PIL import Image, ImageTk +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import threading +import time + + +class WatermarkGUI: + def __init__(self, root, analyzer): + self.root = root + self.analyzer = analyzer + self.source_folder = "" + self.output_folder = "" + self.current_image = None + self.current_preview = None + self.image_files = [] + self.current_index = 0 + self.processing = False + self.analyzing = False + + # 设置中文字体 + self.font_family = "SimHei" # Windows系统默认中文字体 + if os.name == "posix": # macOS/Linux + self.font_family = "WenQuanYi Micro Hei" + + self.create_widgets() + + def create_widgets(self): + """创建GUI组件""" + # 主框架 + main_frame = ttk.Frame(self.root, padding="20") + main_frame.pack(fill=tk.BOTH, expand=True) + + # 顶部信息栏 + info_frame = ttk.Frame(main_frame) + info_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Label(info_frame, text="图片水印分析与去除工具 V9", + font=(self.font_family, 16, "bold")).pack(side=tk.LEFT) + + # 文件夹选择区域 + folder_frame = ttk.LabelFrame(main_frame, text="文件夹设置", padding="10") + folder_frame.pack(fill=tk.X, pady=10) + + # 源文件夹 + ttk.Label(folder_frame, text="源文件夹:", font=(self.font_family, 10)).grid(row=0, column=0, sticky=tk.W, + pady=5) + self.source_entry = ttk.Entry(folder_frame, width=60) + self.source_entry.grid(row=0, column=1, padx=5, pady=5) + ttk.Button(folder_frame, text="浏览...", command=self.browse_source_folder, + style='Accent.TButton').grid(row=0, column=2, padx=5, pady=5) + + # 输出文件夹 + ttk.Label(folder_frame, text="输出文件夹:", font=(self.font_family, 10)).grid(row=1, column=0, sticky=tk.W, + pady=5) + self.output_entry = ttk.Entry(folder_frame, width=60) + self.output_entry.grid(row=1, column=1, padx=5, pady=5) + ttk.Button(folder_frame, text="浏览...", command=self.browse_output_folder, + style='Accent.TButton').grid(row=1, column=2, padx=5, pady=5) + + # 分析和处理区域 + process_frame = ttk.Frame(main_frame) + process_frame.pack(fill=tk.X, pady=10) + + # 左侧分析区域 + analysis_frame = ttk.LabelFrame(process_frame, text="水印分析", padding="10") + analysis_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) + + self.analyze_button = ttk.Button(analysis_frame, text="分析当前图片", command=self.analyze_watermark, + style='Accent.TButton') + self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5) + + self.analysis_status = ttk.Label(analysis_frame, text="未分析水印", + font=(self.font_family, 10)) + self.analysis_status.pack(side=tk.LEFT, padx=10, pady=5) + + # 右侧处理区域 + process_button_frame = ttk.LabelFrame(process_frame, text="处理", padding="10") + process_button_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) + + ttk.Button(process_button_frame, text="去除选中水印", command=self.process_current_image, + style='Accent.TButton').pack(side=tk.LEFT, padx=5, pady=5) + + ttk.Button(process_button_frame, text="批量处理所有图片", command=self.process_all_images, + style='Accent.TButton').pack(side=tk.RIGHT, padx=5, pady=5) + + # 水印选择区域 + selection_frame = ttk.LabelFrame(main_frame, text="选择要去除的水印", padding="10") + selection_frame.pack(fill=tk.X, pady=10) + + # 创建水印列表 + columns = ("type", "x", "y", "width", "height", "confidence", "select") + self.watermark_tree = ttk.Treeview(selection_frame, columns=columns, show="headings", height=4) + + # 设置列标题 + self.watermark_tree.heading("type", text="类型") + self.watermark_tree.heading("x", text="X坐标") + self.watermark_tree.heading("y", text="Y坐标") + self.watermark_tree.heading("width", text="宽度") + self.watermark_tree.heading("height", text="高度") + self.watermark_tree.heading("confidence", text="置信度") + self.watermark_tree.heading("select", text="选择") + + # 设置列宽 + self.watermark_tree.column("type", width=80, anchor=tk.CENTER) + self.watermark_tree.column("x", width=60, anchor=tk.CENTER) + self.watermark_tree.column("y", width=60, anchor=tk.CENTER) + self.watermark_tree.column("width", width=60, anchor=tk.CENTER) + self.watermark_tree.column("height", width=60, anchor=tk.CENTER) + self.watermark_tree.column("confidence", width=80, anchor=tk.CENTER) + self.watermark_tree.column("select", width=60, anchor=tk.CENTER) + + self.watermark_tree.pack(fill=tk.X, pady=5) + + # 选择按钮 + button_frame = ttk.Frame(selection_frame) + button_frame.pack(fill=tk.X, pady=5) + + ttk.Button(button_frame, text="选择所有", command=self.select_all_watermarks).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="取消选择", command=self.deselect_all_watermarks).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="应用选择", command=self.apply_watermark_selection).pack(side=tk.RIGHT, padx=5) + + # 检测参数设置 + params_frame = ttk.LabelFrame(main_frame, text="检测参数设置", padding="10") + params_frame.pack(fill=tk.X, pady=10) + + # 第一行参数 + row1_frame = ttk.Frame(params_frame) + row1_frame.pack(fill=tk.X, pady=5) + + # 文字敏感度 + ttk.Label(row1_frame, text="文字敏感度:").grid(row=0, column=0, sticky=tk.W, pady=5, padx=5) + self.text_sensitivity = tk.IntVar(value=30) + text_scale = ttk.Scale(row1_frame, from_=10, to=100, orient=tk.HORIZONTAL, + variable=self.text_sensitivity, length=200, command=self.on_text_sensitivity_change) + text_scale.grid(row=0, column=1, padx=5, pady=5) + self.text_sensitivity_value = ttk.Label(row1_frame, text=str(self.text_sensitivity.get())) + self.text_sensitivity_value.grid(row=0, column=2, padx=5, pady=5) + + # 印章敏感度 + ttk.Label(row1_frame, text="印章敏感度:").grid(row=0, column=3, sticky=tk.W, pady=5, padx=5) + self.stamp_sensitivity = tk.IntVar(value=30) + stamp_scale = ttk.Scale(row1_frame, from_=10, to=100, orient=tk.HORIZONTAL, + variable=self.stamp_sensitivity, length=200, command=self.on_stamp_sensitivity_change) + stamp_scale.grid(row=0, column=4, padx=5, pady=5) + self.stamp_sensitivity_value = ttk.Label(row1_frame, text=str(self.stamp_sensitivity.get())) + self.stamp_sensitivity_value.grid(row=0, column=5, padx=5, pady=5) + + # 第二行参数 + row2_frame = ttk.Frame(params_frame) + row2_frame.pack(fill=tk.X, pady=5) + + # 显著性阈值 + ttk.Label(row2_frame, text="显著性阈值:").grid(row=0, column=0, sticky=tk.W, pady=5, padx=5) + self.saliency_threshold = tk.DoubleVar(value=self.analyzer.saliency_threshold) + saliency_scale = ttk.Scale(row2_frame, from_=0.1, to=0.9, orient=tk.HORIZONTAL, + variable=self.saliency_threshold, length=200, + command=lambda s: self.saliency_threshold.set(round(float(s), 1))) + saliency_scale.grid(row=0, column=1, padx=5, pady=5) + self.saliency_value = ttk.Label(row2_frame, text=str(self.saliency_threshold.get())) + self.saliency_value.grid(row=0, column=2, padx=5, pady=5) + + # 边缘阈值 + ttk.Label(row2_frame, text="边缘阈值:").grid(row=0, column=3, sticky=tk.W, pady=5, padx=5) + self.edge_threshold = tk.IntVar(value=self.analyzer.edge_threshold) + edge_scale = ttk.Scale(row2_frame, from_=50, to=200, orient=tk.HORIZONTAL, + variable=self.edge_threshold, length=200, command=self.on_edge_threshold_change) + edge_scale.grid(row=0, column=4, padx=5, pady=5) + self.edge_value = ttk.Label(row2_frame, text=str(self.edge_threshold.get())) + self.edge_value.grid(row=0, column=5, padx=5, pady=5) + + # 第三行参数 - 检测方法选择 + row3_frame = ttk.Frame(params_frame) + row3_frame.pack(fill=tk.X, pady=5) + + self.use_color_filter = tk.BooleanVar(value=self.analyzer.use_color_filter) + ttk.Checkbutton(row3_frame, text="颜色过滤", variable=self.use_color_filter, + command=self.on_detection_method_change).grid(row=0, column=0, sticky=tk.W, pady=5, padx=5) + + self.use_mser = tk.BooleanVar(value=self.analyzer.use_mser) + ttk.Checkbutton(row3_frame, text="文字检测(MSER)", variable=self.use_mser, + command=self.on_detection_method_change).grid(row=0, column=1, sticky=tk.W, pady=5, padx=5) + + self.use_texture_analysis = tk.BooleanVar(value=self.analyzer.use_texture_analysis) + ttk.Checkbutton(row3_frame, text="纹理分析", variable=self.use_texture_analysis, + command=self.on_detection_method_change).grid(row=0, column=2, sticky=tk.W, pady=5, padx=5) + + # 修复参数设置 + repair_frame = ttk.LabelFrame(main_frame, text="修复参数设置", padding="10") + repair_frame.pack(fill=tk.X, pady=10) + + # 第一行修复参数 + repair_row1_frame = ttk.Frame(repair_frame) + repair_row1_frame.pack(fill=tk.X, pady=5) + + # 修复半径 + ttk.Label(repair_row1_frame, text="修复半径:").grid(row=0, column=0, sticky=tk.W, pady=5, padx=5) + self.repair_radius = tk.IntVar(value=self.analyzer.inpaint_radius) + repair_scale = ttk.Scale(repair_row1_frame, from_=1, to=15, orient=tk.HORIZONTAL, + variable=self.repair_radius, length=200, command=self.on_repair_radius_change) + repair_scale.grid(row=0, column=1, padx=5, pady=5) + self.repair_radius_value = ttk.Label(repair_row1_frame, text=str(self.repair_radius.get())) + self.repair_radius_value.grid(row=0, column=2, padx=5, pady=5) + + # 修复算法 + ttk.Label(repair_row1_frame, text="修复算法:").grid(row=0, column=3, sticky=tk.W, pady=5, padx=5) + self.repair_algorithm = tk.StringVar(value="TELEA") + algorithm_combo = ttk.Combobox(repair_row1_frame, textvariable=self.repair_algorithm, + values=["TELEA", "NS"], state="readonly", width=10) + algorithm_combo.grid(row=0, column=4, padx=5, pady=5) + algorithm_combo.bind("<>", self.on_repair_algorithm_change) + + # 图像预览区域 + preview_frame = ttk.LabelFrame(main_frame, text="图像预览", padding="10") + preview_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + # 创建画布 + self.canvas_frame = ttk.Frame(preview_frame) + self.canvas_frame.pack(fill=tk.BOTH, expand=True) + + self.canvas = tk.Canvas(self.canvas_frame, bg="white") + self.canvas.pack(fill=tk.BOTH, expand=True) + + # 添加滚动条 + self.h_scrollbar = ttk.Scrollbar(self.canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview) + self.v_scrollbar = ttk.Scrollbar(self.canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview) + + self.canvas.configure(xscrollcommand=self.h_scrollbar.set, yscrollcommand=self.v_scrollbar.set) + + self.h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) + self.v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # 图像导航 + nav_frame = ttk.Frame(main_frame) + nav_frame.pack(fill=tk.X, pady=10) + + ttk.Button(nav_frame, text="上一张", command=self.prev_image).pack(side=tk.LEFT, padx=5) + self.image_info = ttk.Label(nav_frame, text="未加载图片", font=(self.font_family, 10)) + self.image_info.pack(side=tk.LEFT, padx=20) + ttk.Button(nav_frame, text="下一张", command=self.next_image).pack(side=tk.RIGHT, padx=5) + + # 状态栏 + self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W) + self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) + + # 绑定鼠标滚轮事件 + self.canvas.bind("", self.on_mousewheel) # Windows + self.canvas.bind("", self.on_mousewheel) # Linux + self.canvas.bind("", self.on_mousewheel) # Linux + + # 绑定画布大小变化事件 + self.canvas.bind("", self.on_canvas_configure) + + def browse_source_folder(self): + """浏览并选择源文件夹""" + folder = filedialog.askdirectory(title="选择源文件夹") + if folder: + self.source_folder = folder + self.source_entry.delete(0, tk.END) + self.source_entry.insert(0, folder) + self.load_images() + + def browse_output_folder(self): + """浏览并选择输出文件夹""" + folder = filedialog.askdirectory(title="选择输出文件夹") + if folder: + self.output_folder = folder + self.output_entry.delete(0, tk.END) + self.output_entry.insert(0, folder) + + def load_images(self): + """加载文件夹中的所有图像""" + if not self.source_folder: + return + + self.image_files = [] + valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif'] + + for file in os.listdir(self.source_folder): + if any(file.lower().endswith(ext) for ext in valid_extensions): + self.image_files.append(os.path.join(self.source_folder, file)) + + if self.image_files: + self.current_index = 0 + self.load_current_image() + self.update_image_info() + else: + messagebox.showinfo("提示", "所选文件夹中没有找到图像文件") + + def load_current_image(self): + """加载当前选中的图像""" + if not self.image_files: + return + + try: + self.current_image = cv2.imread(self.image_files[self.current_index]) + if self.current_image is None: + messagebox.showerror("错误", f"无法加载图像: {self.image_files[self.current_index]}") + return + + # 重置水印分析结果 + self.analyzer.watermark_regions = [] + self.update_watermark_tree() + self.analysis_status.config(text="未分析水印") + + # 显示图像 + self.display_image() + + except Exception as e: + messagebox.showerror("错误", f"加载图像时出错: {str(e)}") + + def display_image(self): + """在画布上显示当前图像""" + if self.current_image is None: + return + + # 复制图像用于显示 + display_img = self.current_image.copy() + + # 如果有选中的水印区域,在预览图上标记 + for region in self.analyzer.selected_regions: + x, y, w, h = region['position'] + color = (0, 255, 0) # 绿色边框 + cv2.rectangle(display_img, (x, y), (x + w, y + h), color, 2) + + # 转换为PIL格式 + rgb_img = cv2.cvtColor(display_img, cv2.COLOR_BGR2RGB) + pil_img = Image.fromarray(rgb_img) + + # 调整图像大小以适应画布 + canvas_width = self.canvas.winfo_width() - 20 + canvas_height = self.canvas.winfo_height() - 20 + + if canvas_width <= 0 or canvas_height <= 0: + return + + img_width, img_height = pil_img.size + + # 计算缩放比例 + scale = min(canvas_width / img_width, canvas_height / img_height) + + if scale < 1: + pil_img = pil_img.resize((int(img_width * scale), int(img_height * scale)), Image.LANCZOS) + + # 转换为Tkinter可用的格式 + self.current_preview = ImageTk.PhotoImage(image=pil_img) + + # 在画布上显示图像 + self.canvas.delete("all") + self.canvas.create_image(10, 10, anchor=tk.NW, image=self.current_preview) + + # 更新画布滚动区域 + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def update_image_info(self): + """更新图像信息标签""" + if not self.image_files: + self.image_info.config(text="未加载图片") + return + + self.image_info.config( + text=f"图片 {self.current_index + 1}/{len(self.image_files)}: {os.path.basename(self.image_files[self.current_index])}") + + def prev_image(self): + """显示上一张图片""" + if not self.image_files: + return + + self.current_index = (self.current_index - 1) % len(self.image_files) + self.load_current_image() + self.update_image_info() + + def next_image(self): + """显示下一张图片""" + if not self.image_files: + return + + self.current_index = (self.current_index + 1) % len(self.image_files) + self.load_current_image() + self.update_image_info() + + def analyze_watermark(self): + """分析当前图片中的水印""" + if self.current_image is None: + messagebox.showinfo("提示", "请先加载图片") + return + + if self.analyzing: + messagebox.showinfo("提示", "正在进行水印分析,请稍候...") + return + + # 禁用分析按钮 + self.analyze_button.config(state=tk.DISABLED) + self.analysis_status.config(text="分析中...") + self.analyzing = True + + # 更新分析参数 + self.update_analysis_parameters() + + # 在单独的线程中执行水印分析 + analysis_thread = threading.Thread(target=self._analyze_watermark_thread) + analysis_thread.daemon = True + analysis_thread.start() + + def _analyze_watermark_thread(self): + """在后台线程中执行水印分析""" + try: + # 执行水印分析 + start_time = time.time() + self.analyzer.analyze(self.current_image) + analysis_time = time.time() - start_time + + # 更新UI + self.root.after(0, self._update_after_analysis, analysis_time) + + except Exception as e: + self.root.after(0, messagebox.showerror, "错误", f"分析水印时出错: {str(e)}") + self.root.after(0, self._reset_analysis_state) + + def _update_after_analysis(self, analysis_time): + """分析完成后更新UI""" + # 更新水印列表 + self.update_watermark_tree() + + # 更新状态 + if self.analyzer.watermark_regions: + self.analysis_status.config( + text=f"已检测到 {len(self.analyzer.watermark_regions)} 个水印区域 (耗时: {analysis_time:.2f}秒)") + else: + self.analysis_status.config(text="未检测到水印 (耗时: {analysis_time:.2f}秒)") + + # 重新显示图像(包含水印标记) + self.display_image() + + # 重置分析状态 + self._reset_analysis_state() + + def _reset_analysis_state(self): + """重置分析状态""" + self.analyze_button.config(state=tk.NORMAL) + self.analyzing = False + + def update_watermark_tree(self): + """更新水印树状视图""" + # 清空现有项 + for item in self.watermark_tree.get_children(): + self.watermark_tree.delete(item) + + # 添加新项 + for i, region in enumerate(self.analyzer.watermark_regions): + x, y, w, h = region['position'] + confidence = region['confidence'] + r_type = region['type'] + + # 判断是否选中 + is_selected = region in self.analyzer.selected_regions + select_text = "✔" if is_selected else "" + + self.watermark_tree.insert("", tk.END, values=( + r_type, x, y, w, h, f"{confidence:.2f}", select_text + )) + + def select_all_watermarks(self): + """选择所有水印区域""" + self.analyzer.selected_regions = self.analyzer.watermark_regions.copy() + self.update_watermark_tree() + self.display_image() + + def deselect_all_watermarks(self): + """取消选择所有水印区域""" + self.analyzer.selected_regions = [] + self.update_watermark_tree() + self.display_image() + + def apply_watermark_selection(self): + """应用水印选择(基于用户在表格中的选择)""" + selected_regions = [] + + for item_id in self.watermark_tree.selection(): + item = self.watermark_tree.item(item_id) + values = item['values'] + + if not values: + continue + + # 查找对应的区域 + for region in self.analyzer.watermark_regions: + x, y, w, h = region['position'] + if (int(values[1]) == x and int(values[2]) == y and + int(values[3]) == w and int(values[4]) == h): + selected_regions.append(region) + break + + self.analyzer.selected_regions = selected_regions + self.update_watermark_tree() + self.display_image() + + def process_current_image(self): + """处理当前图片,去除选中的水印""" + if self.current_image is None: + messagebox.showinfo("提示", "请先加载图片") + return + + if not self.analyzer.selected_regions: + messagebox.showinfo("提示", "请先选择要去除的水印区域") + return + + if self.processing: + messagebox.showinfo("提示", "正在处理图片,请稍候...") + return + + # 更新修复参数 + self.update_repair_parameters() + + # 禁用处理按钮 + self.processing = True + self.status_bar.config(text="处理中...") + + # 在单独的线程中执行图片处理 + process_thread = threading.Thread(target=self._process_current_image_thread) + process_thread.daemon = True + process_thread.start() + + def _process_current_image_thread(self): + """在后台线程中处理当前图片""" + try: + # 处理图片 + result = self.analyzer.remove_watermarks(self.current_image) + + # 保存结果 + if self.output_folder: + output_path = os.path.join(self.output_folder, + f"processed_{os.path.basename(self.image_files[self.current_index])}") + cv2.imwrite(output_path, result) + self.status_bar.config(text=f"已保存处理后的图片: {output_path}") + else: + self.status_bar.config(text="处理完成,但未指定输出文件夹,结果未保存") + + # 更新当前图片为处理后的结果 + self.current_image = result + + # 更新UI + self.root.after(0, self.display_image) + self.root.after(0, self._reset_processing_state) + + except Exception as e: + self.root.after(0, messagebox.showerror, "错误", f"处理图片时出错: {str(e)}") + self.root.after(0, self._reset_processing_state) + + def _reset_processing_state(self): + """重置处理状态""" + self.processing = False + self.status_bar.config(text="就绪") + + def process_all_images(self): + """批量处理所有图片""" + if not self.image_files: + messagebox.showinfo("提示", "请先加载图片") + return + + if not self.output_folder: + messagebox.showinfo("提示", "请先选择输出文件夹") + return + + if self.processing: + messagebox.showinfo("提示", "正在处理图片,请稍候...") + return + + # 更新修复参数 + self.update_repair_parameters() + + # 确认对话框 + result = messagebox.askyesno("确认", f"确定要批量处理所有 {len(self.image_files)} 张图片吗?") + if not result: + return + + # 禁用处理按钮 + self.processing = True + self.status_bar.config(text="批量处理中...") + + # 在单独的线程中执行批量处理 + batch_thread = threading.Thread(target=self._batch_process_thread) + batch_thread.daemon = True + batch_thread.start() + + def _batch_process_thread(self, i=None): + """在后台线程中执行批量处理""" + try: + total = len(self.image_files) + processed = 0 + errors = 0 + + for i, img_path in enumerate(self.image_files): + # 更新状态栏 + self.root.after(0, lambda i=i: self.status_bar.config(text=f"处理中 ({i + 1}/{total})...")) + + try: + # 读取图片 + img = cv2.imread(img_path) + if img is None: + errors += 1 + continue + + # 分析水印 + self.analyzer.analyze(img) + + # 处理图片 + if self.analyzer.watermark_regions: + result = self.analyzer.remove_watermarks(img) + + # 保存结果 + output_path = os.path.join(self.output_folder, + f"processed_{os.path.basename(img_path)}") + cv2.imwrite(output_path, result) + + processed += 1 + + except Exception as e: + print(f"处理图片 {img_path} 时出错: {str(e)}") + errors += 1 + + # 更新UI + self.root.after(0, lambda: messagebox.showinfo("完成", + f"批量处理完成!\n成功: {processed}\n失败: {errors}")) + self.root.after(0, lambda: self.status_bar.config(text=f"批量处理完成: 成功 {processed}, 失败 {errors}")) + self.root.after(0, self._reset_processing_state) + + except Exception as e: + self.root.after(0, messagebox.showerror, "错误", f"批量处理时出错: {str(e)}") + self.root.after(0, self._reset_processing_state) + + def update_analysis_parameters(self): + """更新分析参数""" + self.analyzer.text_threshold = self.text_sensitivity.get() + self.analyzer.stamp_threshold = self.stamp_sensitivity.get() + self.analyzer.saliency_threshold = self.saliency_threshold.get() + self.analyzer.edge_threshold = self.edge_threshold.get() + self.analyzer.use_color_filter = self.use_color_filter.get() + self.analyzer.use_mser = self.use_mser.get() + self.analyzer.use_texture_analysis = self.use_texture_analysis.get() + + def update_repair_parameters(self): + """更新修复参数""" + self.analyzer.inpaint_radius = self.repair_radius.get() + + if self.repair_algorithm.get() == "TELEA": + self.analyzer.inpaint_algorithm = cv2.INPAINT_TELEA + else: + self.analyzer.inpaint_algorithm = cv2.INPAINT_NS + + def on_text_sensitivity_change(self, event): + """文字敏感度滑块变化事件""" + value = self.text_sensitivity.get() + self.text_sensitivity_value.config(text=str(value)) + + def on_stamp_sensitivity_change(self, event): + """印章敏感度滑块变化事件""" + value = self.stamp_sensitivity.get() + self.stamp_sensitivity_value.config(text=str(value)) + + def on_edge_threshold_change(self, event): + """边缘阈值滑块变化事件""" + value = self.edge_threshold.get() + self.edge_value.config(text=str(value)) + + def on_repair_radius_change(self, event): + """修复半径滑块变化事件""" + value = self.repair_radius.get() + self.repair_radius_value.config(text=str(value)) + + def on_repair_algorithm_change(self, event): + """修复算法选择变化事件""" + pass + + def on_detection_method_change(self): + """检测方法选择变化事件""" + pass + + def on_mousewheel(self, event): + """鼠标滚轮事件处理""" + if event.num == 4 or event.delta > 0: # 向上滚动 + self.canvas.yview_scroll(-1, "units") + elif event.num == 5 or event.delta < 0: # 向下滚动 + self.canvas.yview_scroll(1, "units") + + def on_canvas_configure(self, event): + """画布大小变化事件处理""" + self.display_image() + + +def run_gui(): + """运行GUI应用""" + # 尝试导入Analyzer类 + try: + from analyzer import WatermarkAnalyzer + analyzer = WatermarkAnalyzer() + except ImportError: + # 如果无法导入,创建一个模拟类用于演示 + class WatermarkAnalyzer: + def __init__(self): + self.watermark_regions = [] + self.selected_regions = [] + self.inpaint_radius = 5 + self.inpaint_algorithm = cv2.INPAINT_TELEA + self.text_threshold = 180 + self.stamp_threshold = 180 + self.saliency_threshold = 0.5 + self.edge_threshold = 100 + self.use_color_filter = True + self.use_mser = True + self.use_texture_analysis = True + + def analyze(self, image): + # 模拟水印分析 + height, width = image.shape[:2] + self.watermark_regions = [ + { + 'type': 'text', + 'position': (width // 2 - 50, 10, 100, 30), + 'confidence': 0.8, + 'contour': None + }, + { + 'type': 'stamp', + 'position': (width - 100, height - 100, 80, 80), + 'confidence': 0.9, + 'contour': None + } + ] + self.selected_regions = self.watermark_regions.copy() + + def remove_watermarks(self, image): + # 模拟水印去除 + result = image.copy() + for region in self.selected_regions: + x, y, w, h = region['position'] + # 创建掩码 + mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.uint8) + mask[y:y + h, x:x + w] = 255 + # 使用修复算法去除水印 + result = cv2.inpaint(result, mask, self.inpaint_radius, self.inpaint_algorithm) + return result + + analyzer = WatermarkAnalyzer() + + # 创建主窗口 + root = tk.Tk() + root.title("图片水印分析与去除工具 V9") + root.geometry("1000x800") + + # 设置样式 + style = ttk.Style() + style.configure('Accent.TButton', font=(None, 10, 'bold')) + + # 创建GUI + app = WatermarkGUI(root, analyzer) + + # 启动主循环 + root.mainloop() + + +if __name__ == "__main__": + run_gui() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d1bb32e --- /dev/null +++ b/main.py @@ -0,0 +1,24 @@ +import os +import sys +from tkinter import Tk +from gui import WatermarkGUI +from analyzer import WatermarkAnalyzer + + +def main(): + # 创建配置目录 + config_dir = "config" + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 初始化水印分析器 + analyzer = WatermarkAnalyzer() + + # 创建GUI + root = Tk() + app = WatermarkGUI(root, analyzer) + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d78cfbc --- /dev/null +++ b/utils.py @@ -0,0 +1,63 @@ +import os +import cv2 +import numpy as np + + +def create_output_folder(source_folder, suffix="_processed"): + """创建输出文件夹""" + base_name = os.path.basename(source_folder) + parent_dir = os.path.dirname(source_folder) + output_folder = os.path.join(parent_dir, f"{base_name}{suffix}") + + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + return output_folder + + +def is_image_file(file_path): + """检查文件是否为图片""" + valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif'] + ext = os.path.splitext(file_path)[1].lower() + return ext in valid_extensions + + +def resize_to_fit(image, max_width, max_height): + """调整图片大小以适应指定尺寸""" + height, width = image.shape[:2] + + # 计算宽高比 + ratio = min(max_width / width, max_height / height) + + # 计算新尺寸 + new_width = int(width * ratio) + new_height = int(height * ratio) + + # 调整大小 + return cv2.resize(image, (new_width, new_height)) + + +def draw_watermark_regions(image, regions): + """在图片上绘制水印区域""" + result = image.copy() + + for region in regions: + x, y, w, h = region['position'] + + # 根据区域类型选择颜色 + if region['type'] == 'text': + color = (255, 0, 0) # 蓝色 + elif region['type'] == 'stamp': + color = (0, 0, 255) # 红色 + else: + color = (0, 255, 0) # 绿色 + + # 绘制矩形 + cv2.rectangle(result, (x, y), (x + w, y + h), color, 2) + + # 添加标签 + label = f"{region['type']} ({region['confidence']:.2f})" + cv2.putText(result, label, (x, y - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) + + return result \ No newline at end of file diff --git a/watermark_config.json b/watermark_config.json new file mode 100644 index 0000000..0ea3bc1 --- /dev/null +++ b/watermark_config.json @@ -0,0 +1 @@ +{"position": [205, 289, 1469, 1784], "timestamp": "2025-05-22T19:49:32.415955", "inpaint_radius": 5, "inpaint_algorithm": 1, "dilate_size": 3, "use_adaptive_mask": true, "text_threshold": 180, "min_text_area": 100, "text_color_range": [[50, 50, 50], [180, 180, 180]], "stamp_color_range": [[0, 0, 100], [100, 100, 255]]} \ No newline at end of file