手写签名提取工具
—————【下 载 地 址】———————
【本章下载一】:https://pan.xunlei.com/s/VOVArK01pMTMYZHtp2YVURGYA1?pwd=vcza#
【本章下载二】:https://pan.xunlei.com/s/VOVArK01pMTMYZHtp2YVURGYA1?pwd=vcza#
【百款黑科技】:https://ucnygalh6wle.feishu.cn/wiki/HPQywvPc7iLZu1k0ODFcWMt2n0d?from=from_copylink
—————【下 载 地 址】———————
前情提要,昨天发布了一个将文字转成模拟手写的主题,有人要“ai抠图签名图片、输出为png和cad格式”,我正好也要用到此类的功能,虽然说网上有大量的在线网站可以实现这个,但是不能批量实现。
比喻说我一次找10个人签名,签在一张A4的纸张上面,常规做法就是签完扫描后丢到ps里面去处理抠图,然后再一个一个保存,这个我也会,但是还是觉得麻烦。于是我又让ai帮我写一个可以实现我这个需求的程序。
不得不感叹,现在AI真的是强大,你只管提需求,剩下的交给AI。
[b]我是基于我的需求开发的,另外纯小白一个,勿喷,都是ai写的,我提的需求。
【分享】签名提取工具
本工具是一款支持手动多区域框选、智能图像优化处理、一键自适应分离签名PNG透明底的高级签名提取软件。针对高分辨率扫描件和复杂背景文件,内置高效算法,可显著提升签名分割质量。自带EXE版本,无需安装Python环境,开箱即用!
功能特色
支持多区域橡皮筋拖选签名,一次处理多份签名,解放批量操作
CLAHE对比度增强 + 智能白底优化 + 笔迹加深,极大提升低对比签名提取效果
图像预览支持缩放/拖拽/精准定位,大图不卡顿,人性化体验
批量输出透明PNG文件,无水印、无广告
极简GUI操作,无需任何命令行基础
多线程加速处理,不卡死不卡白
已打包为单文件EXE,即点即用!
使用方法
1运行gui_main.exe;
2选择需要处理的扫描图片;
3可以切换“高级优化”进行图像质量提升,推荐优化后再选区域;
4鼠标滚轮缩放图像,中键拖动画布定位,左键框选签名区域(支持多选,多区域编号);
5点击“提取签名”,软件将智能分割各区域签名,自动输出到指定文件夹。
核心源码
gui_main.py
复制代码 隐藏代码
import tkinter as tk
from src.gui_interface import create_guiif __name__ == '__main__':create_gui()
settings.py
复制代码 隐藏代码
# config/settings.py
# ==================== 输出设置 ====================
OUTPUT = {'output_dir': 'extracted_signatures', # 输出目录'prefix': 'signature_' # 文件前缀
}
# ==================== 签名提取设置 ====================
SIGNATURE_EXTRACTION = {'signature_padding': 20, # 签名周围的填充空间'selection_color': (255, 0, 0), # 选区框颜色 (BGR)'adaptive_threshold_block': 21, # 自适应阈值块大小(应为奇数)'adaptive_threshold_c': 5, # 自适应阈值常数'min_alpha_value': 20, # 最小有效alpha值'max_channel_difference': 15, # 最大允许的通道差异'stroke_enhance_strength': 0.8, # 笔画增强强度 (0-1)'max_stroke_thickness': 8, # 最大笔画厚度(像素)'min_stroke_length': 10, # 最小笔画长度(像素)'preprocess_steps': {'auto_enhance': True, # 是否启用自动增强'background_whiten': True, # 是否漂白背景'signature_darken': True, # 是否加深签名'contrast_level': 1.5, # 对比度增强级别 (1-3)'whiten_strength': 0.9, # 漂白强度 (0-1)'darken_strength': 1.2, # 加深强度 (>1)'use_clahe': True, # <---- 新增'use_advanced_preprocessing': True # <---- 新增}
}
gui_interface.py
复制代码 隐藏代码
import os
import cv2
import numpy as np
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import threadingtry:from ttkthemes import ThemedTk
except ImportError:ThemedTk = Nonefrom config import settings# --------- 工具函数 ----------
def ensure_dir(directory):if not os.path.exists(directory):os.makedirs(directory)def is_valid_image_file(file_path):valid_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.tif']ext = os.path.splitext(file_path)[1].lower()return ext in valid_extensionsdef load_image(file_path):if not os.path.exists(file_path):return Nonetry:image = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), 1)if image is None:raise IOError("无法读取图像文件")return imageexcept Exception as e:print(f"加载图像错误: {str(e)}")return None# --------- 核心图像处理 ----------
class SignatureExtractor:def __init__(self):self.output_settings = settings.OUTPUTself.extraction_settings = settings.SIGNATURE_EXTRACTIONdef extract_selected_signatures(self, input_path, selections, output_dir=None, use_enhanced_image=True):if not os.path.exists(input_path):print(f"错误: 文件不存在 - {input_path}")return False, [], Noneif not is_valid_image_file(input_path):print(f"错误: 不支持的图像格式 - {input_path}")return False, [], Noneif not selections:print("没有选择任何签名区域")return False, [], Noneif output_dir is None:output_dir = self.output_settings['output_dir']ensure_dir(output_dir)orig_image = load_image(input_path)if orig_image is None:print("无法加载图像!")return False, [], Noneif use_enhanced_image:try:enhanced_image = self.enhance_image_advanced(orig_image.copy())except Exception as e:print(f"图像优化失败: {e}")enhanced_image = orig_image.copy()else:enhanced_image = orig_image.copy()signature_paths = []base_filename = os.path.splitext(os.path.basename(input_path))[0]for i, region in enumerate(selections):region_image = self._extract_region(orig_image, region)if region_image is None:print(f"选区 {i+1} 无效")continuetry:signature_img = self._extract_and_optimize_signature_advanced(region_image)except Exception as e:print(f"提取签名时出错: {e}")continueif signature_img is not None and not self._is_mostly_transparent(signature_img):output_filename = f"{self.output_settings['prefix']}{base_filename}_{i+1}.png"output_path = os.path.join(output_dir, output_filename)cv2.imencode('.png', signature_img)[1].tofile(output_path)signature_paths.append(output_path)print(f"签名已保存: {output_path}")else:print(f"警告: 选区 {i+1} 提取失败 - 可能签名过浅或区域不包含有效签名")return True if signature_paths else False, signature_paths, enhanced_imagedef enhance_image_advanced(self, image):if len(image.shape) == 2:image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)try:denoised = cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)except Exception:denoised = imagelab = cv2.cvtColor(denoised, cv2.COLOR_BGR2LAB)l_channel, a_channel, b_channel = cv2.split(lab)use_clahe = self.extraction_settings.get('preprocess_steps', {}).get('use_clahe', True)if use_clahe:clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))l_enhanced = clahe.apply(l_channel)else:l_enhanced = l_channell_stretched = cv2.normalize(l_enhanced, None, 0, 255, cv2.NORM_MINMAX)enhanced_lab = cv2.merge([l_stretched, a_channel, b_channel])enhanced_bgr = cv2.cvtColor(enhanced_lab, cv2.COLOR_LAB2BGR)enhanced_bgr = self._enhance_white_background(enhanced_bgr)enhanced_bgr = self._darken_signature_strokes(enhanced_bgr)kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])sharpened = cv2.filter2D(enhanced_bgr, -1, kernel)final_result = cv2.addWeighted(enhanced_bgr, 0.7, sharpened, 0.3, 0)return final_resultdef _enhance_white_background(self, image):gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))background_mask = cv2.morphologyEx(binary, cv2.MORPH_DILATE, kernel)enhanced = image.copy().astype(np.float32)background_indices = background_mask == 255enhanced[background_indices] = enhanced[background_indices] * 1.2 + 30enhanced = np.clip(enhanced, 0, 255).astype(np.uint8)return enhanceddef _darken_signature_strokes(self, image):hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)h, s, v = cv2.split(hsv)gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)adaptive_thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))stroke_mask = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_CLOSE, kernel)darkened = image.copy().astype(np.float32)stroke_indices = stroke_mask == 255darkened[stroke_indices] = darkened[stroke_indices] * 0.6darkened = np.clip(darkened, 0, 255).astype(np.uint8)return darkeneddef _extract_region(self, image, region):x1, y1, x2, y2 = regionx_min = min(int(x1), int(x2))y_min = min(int(y1), int(y2))x_max = max(int(x1), int(x2))y_max = max(int(y1), int(y2))padding = self.extraction_settings.get('signature_padding', 20)x_min = max(0, x_min - padding)y_min = max(0, y_min - padding)x_max = min(image.shape[1], x_max + padding)y_max = min(image.shape[0], y_max + padding)if x_min >= x_max or y_min >= y_max:return Nonereturn image[y_min:y_max, x_min:x_max]def _extract_and_optimize_signature_advanced(self, region_image):processed = self._preprocess_region(region_image)edges = self._multi_scale_edge_detection(processed)binary_mask = self._intelligent_thresholding(processed)combined_mask = cv2.bitwise_or(edges, binary_mask)optimized_mask = self._morphological_optimization(combined_mask)result = self._create_high_quality_transparent_image(region_image, optimized_mask)return resultdef _preprocess_region(self, image):denoised = cv2.bilateralFilter(image, 9, 75, 75)gray = cv2.cvtColor(denoised, cv2.COLOR_BGR2GRAY)clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))enhanced = clahe.apply(gray)return enhanceddef _multi_scale_edge_detection(self, gray_image):scales = [1.0, 1.5, 2.0]edges_list = []for scale in scales:sigma = scaleblurred = cv2.GaussianBlur(gray_image, (0, 0), sigma)edges = cv2.Canny(blurred, 50, 150)edges_list.append(edges)final_edges = np.zeros_like(gray_image)for edges in edges_list:final_edges = cv2.bitwise_or(final_edges, edges)return final_edgesdef _intelligent_thresholding(self, gray_image):adaptive1 = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2)_, otsu = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)_, triangle = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_TRIANGLE)combined = cv2.bitwise_or(adaptive1, otsu)combined = cv2.bitwise_or(combined, triangle)return combineddef _morphological_optimization(self, binary_mask):kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))cleaned = cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel_small)kernel_connect = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))connected = cv2.morphologyEx(cleaned, cv2.MORPH_CLOSE, kernel_connect)contours, _ = cv2.findContours(connected, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)min_area = 50final_mask = np.zeros_like(binary_mask)for contour in contours:if cv2.contourArea(contour) > min_area:cv2.drawContours(final_mask, [contour], -1, 255, -1)return final_maskdef _create_high_quality_transparent_image(self, original_image, mask):height, width = mask.shapetransparent_image = np.zeros((height, width, 4), dtype=np.uint8)for c in range(3):transparent_image[:, :, c] = original_image[:, :, c]transparent_image[:, :, 3] = maskalpha_channel = transparent_image[:, :, 3].astype(np.float32)alpha_blurred = cv2.GaussianBlur(alpha_channel, (3, 3), 0.5)transparent_image[:, :, 3] = alpha_blurred.astype(np.uint8)return transparent_imagedef _is_mostly_transparent(self, image):if image is None or len(image.shape) < 3 or image.shape[2] < 4:return Truealpha_channel = image[..., 3]non_transparent_pixels = np.sum(alpha_channel > self.extraction_settings.get('min_alpha_value', 20))total_pixels = alpha_channel.sizereturn non_transparent_pixels / total_pixels < 0.05# --------- 优化后的缩放+拖拽画布组件 -----------
class ZoomableSelectionCanvas(tk.Canvas):def __init__(self, master, **kwargs):super().__init__(master, **kwargs)self.zoom = 1.0self.offset_x = 0self.offset_y = 0self.min_zoom = 0.15self.max_zoom = 8self.image_pil = Noneself.image_tk = Noneself.image_item = Noneself.selections = []self.dragging = Falseself.start_imgxy = Noneself.temp_rect = None# --- 新增优化缓存 ---self._pending_zoom_after = Noneself._current_zoom_target = self.zoomself._img_cache = {}self._move_start = Noneself.bind("<MouseWheel>", self.on_zoom)self.bind("<ButtonPress-1>", self.on_left_down)self.bind("<B1-Motion>", self.on_left_drag)self.bind("<ButtonRelease-1>", self.on_left_up)# 拖拽支持:鼠标中键 self.bind("<ButtonPress-2>", self.on_middle_down)self.bind("<B2-Motion>", self.on_middle_drag)self.bind("<ButtonRelease-2>", self.on_middle_up)def show_image(self, pil_image):self.image_pil = pil_imageself.zoom = 1.0self.offset_x = 0self.offset_y = 0self._img_cache.clear()self.redraw()def img2canvas(self, x, y):return x * self.zoom + self.offset_x, y * self.zoom + self.offset_ydef canvas2img(self, cx, cy):return (cx - self.offset_x) / self.zoom, (cy - self.offset_y) / self.zoomdef redraw(self):self.delete("all")if self.image_pil is None:returnw, h = self.image_pil.sizedisp_w, disp_h = max(1, int(w * self.zoom)), max(1, int(h * self.zoom))cache_key = f"{disp_w}x{disp_h}"if self._img_cache.get(cache_key):self.image_tk = self._img_cache[cache_key]else:disp_img = self.image_pil.resize((disp_w, disp_h), Image.LANCZOS)self.image_tk = ImageTk.PhotoImage(disp_img)self._img_cache[cache_key] = self.image_tkself.image_item = self.create_image(self.offset_x, self.offset_y, anchor=tk.NW, image=self.image_tk)for i, (x1, y1, x2, y2) in enumerate(self.selections):p1 = self.img2canvas(x1, y1)p2 = self.img2canvas(x2, y2)self.create_rectangle(p1[0], p1[1], p2[0], p2[1], outline='red', width=2, tags='selection')self.create_text(p1[0]+10,p1[1]+10,text=f"{i+1}",fill='red',font=('Arial',12,'bold'),tags='selection')if self.temp_rect:self.lift(self.temp_rect)def on_zoom(self, event):def real_zoom():self.zoom = self._current_zoom_targetself.redraw()self._pending_zoom_after = Noneif not self.image_pil: returnfactor = 1.1 if event.delta > 0 else 0.9self._current_zoom_target = min(max(self.zoom * factor, self.min_zoom), self.max_zoom)mx, my = self.canvasx(event.x), self.canvasy(event.y)ix, iy = self.canvas2img(mx, my)mx2, my2 = self.img2canvas(ix, iy)self.offset_x += (mx - mx2)self.offset_y += (my - my2)if self._pending_zoom_after:self.after_cancel(self._pending_zoom_after)self._pending_zoom_after = self.after(40, real_zoom)def on_left_down(self, event):if not self.image_pil: returnself.dragging = Trueimg_x, img_y = self.canvas2img(event.x, event.y)self.start_imgxy = (img_x, img_y)def on_left_drag(self, event):if not self.dragging or not self.image_pil: returnif self.temp_rect:self.delete(self.temp_rect)x0, y0 = self.start_imgxyx1, y1 = self.canvas2img(event.x, event.y)p0 = self.img2canvas(x0,y0)p1 = (event.x, event.y)self.temp_rect = self.create_rectangle(p0[0], p0[1], p1[0], p1[1], outline='yellow', dash=(4,2), width=2)def on_left_up(self, event):self.delete(self.temp_rect)self.temp_rect = Noneif not self.dragging or not self.image_pil: returnx0, y0 = self.start_imgxyx1, y1 = self.canvas2img(event.x, event.y)if abs(x1-x0) > 10 and abs(y1-y0) > 10:self.selections.append((int(x0), int(y0), int(x1), int(y1)))self.redraw()self.event_generate('<<SelectionCreated>>')self.dragging = False# ------- 画布拖拽 -------def on_middle_down(self, event):self.config(cursor="fleur")self._move_start = (event.x, event.y)def on_middle_drag(self, event):if self._move_start is None: returndx = event.x - self._move_start[0]dy = event.y - self._move_start[1]self.offset_x += dxself.offset_y += dyself._move_start = (event.x, event.y)self.redraw()def on_middle_up(self, event):self.config(cursor="")self._move_start = Nonedef get_selections(self):return list(self.selections)def clear_selections(self):self.selections = []self.redraw()def reset_view(self):self.zoom = 1.0self.offset_x = 0self.offset_y = 0self.redraw()# --------- 主界面入口函数 ----------
def create_gui():root = ThemedTk(theme="arc") if ThemedTk else tk.Tk()root.title("交互式签名提取工具 - 增强版 v2.1")sw, sh = root.winfo_screenwidth(), root.winfo_screenheight()w, h = min(1280, int(sw*0.8)), min(900, int(sh*0.8))root.geometry(f"{w}x{h}")root.minsize(950, 620)root.configure(bg="#F9F9F9")main_frame = ttk.Frame(root, padding=15)main_frame.grid(row=0, column=0, sticky="nsew")root.grid_rowconfigure(0, weight=1)root.grid_columnconfigure(0, weight=1)main_frame.grid_rowconfigure(3, weight=1)main_frame.grid_columnconfigure(0, weight=1)control_frame = ttk.LabelFrame(main_frame, text="文件选择与图像优化", padding=(10,8,10,8))control_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5, columnspan=2)control_frame.grid_columnconfigure(0, weight=1)input_entry = ttk.Entry(control_frame, width=50, font=("Segoe UI", 12))output_entry = ttk.Entry(control_frame, width=50, font=("Segoe UI", 12))optimize_var = tk.BooleanVar(value=True)optimize_cb = ttk.Checkbutton(control_frame, text="启用高级优化", variable=optimize_var, style="Toolbutton")optimize_status = ttk.Label(control_frame, text="(CLAHE增强, 智能白底, 笔迹加深)", font=("Segoe UI", 11,"italic"), foreground="#666")optimize_btn = ttk.Button(control_frame, text="应用优化", width=13)original_btn = ttk.Button(control_frame, text="原始图像", width=13)zoom_info = ttk.Label(control_frame, text="缩放: 100% | 滚轮缩放", foreground="#447")reset_zoom_btn = ttk.Button(control_frame, text="重置视图", width=12)ttk.Label(control_frame, text="输入图像:", font=("Segoe UI",11)).grid(row=0, column=0, sticky="e")input_entry.grid(row=0, column=1, sticky="ew")ttk.Button(control_frame, text="浏览", command=lambda: load_image_file(input_entry)).grid(row=0, column=2, padx=5)ttk.Label(control_frame, text="输出目录:", font=("Segoe UI",11)).grid(row=1, column=0, sticky="e")output_entry.grid(row=1, column=1, sticky="ew")output_entry.insert(0, 'extracted_signatures')ttk.Button(control_frame, text="浏览", command=lambda: select_output_dir(output_entry)).grid(row=1, column=2, padx=5)optimize_cb.grid(row=2, column=0, sticky="w", pady=(7,2))optimize_status.grid(row=2, column=1, sticky="w", padx=(5,0))optimize_btn.grid(row=2, column=2, sticky="e", pady=(7,2))original_btn.grid(row=3, column=2, sticky="e")zoom_info.grid(row=3, column=0, sticky="w", padx=(5,0))reset_zoom_btn.grid(row=3, column=1, sticky="e", padx=(6,0))selection_frame = ttk.LabelFrame(main_frame, text="签名选择与操作", padding=9)selection_frame.grid(row=1, column=0, sticky="ew", pady=5, padx=2)selection_instruction = ("操作说明:\n"" ① 加载图像后,点击『应用优化』提升质量;\n"" ② 鼠标滚轮缩放,中键拖拽定位,左键橡皮筋框选签名(支持多选);\n"" ③ 右菜单可清除/删除/重新选择,底部点击『提取签名』完成智能分割。")ttk.Label(selection_frame, text=selection_instruction, font=("微软雅黑", 10), foreground="#5A5A5A").grid(row=0, column=0, sticky="w")selection_listbox = tk.Listbox(selection_frame, width=75, height=4, font=("Consolas",11))selection_listbox.grid(row=1, column=0, sticky="ew", pady=3)preview_frame = ttk.Frame(main_frame, relief="flat")preview_frame.grid(row=2, column=0, sticky="nsew", padx=0, pady=3)main_frame.grid_rowconfigure(2, weight=2)preview_frame.grid_rowconfigure(0, weight=1)preview_frame.grid_columnconfigure(0, weight=1)orig_frame = ttk.LabelFrame(preview_frame, text="图像预览(滚轮缩放、中键拖拽、左键框选)", padding=5)orig_frame.grid(row=0, column=0, sticky="nsew")orig_frame.grid_rowconfigure(0, weight=1)orig_frame.grid_columnconfigure(0, weight=1)orig_canvas = ZoomableSelectionCanvas(orig_frame, bg="#FAFAFF", highlightthickness=0)orig_canvas.grid(row=0, column=0, sticky="nsew")action_frame = ttk.Frame(main_frame, padding=5)action_frame.grid(row=4, column=0, sticky="ew")clear_btn = ttk.Button(action_frame, text="清除选区", width=15)remove_btn = ttk.Button(action_frame, text="移除所选", width=15)extract_btn = ttk.Button(action_frame, text="提取签名", width=15)help_btn = ttk.Button(action_frame, text="使用指南", width=12)clear_btn.pack(side=tk.LEFT, padx=5)remove_btn.pack(side=tk.LEFT, padx=5)extract_btn.pack(side=tk.LEFT, padx=5)help_btn.pack(side=tk.LEFT, padx=5)status_bar = ttk.Label(root, text="高级签名提取工具已就绪 - 请加载图像开始操作", anchor=tk.W, font=("微软雅黑", 10, "italic"), relief=tk.SUNKEN)status_bar.grid(row=5, column=0, sticky="ew")###### 变量/对象区orig_pil_image, orig_image_cv, enhanced_image = None, None, Nonecurrent_displayed_image = Noneuse_enhanced_image = Trueextractor = SignatureExtractor()###### 内部函数def update_selection_listbox():selection_listbox.delete(0, tk.END)for i, (x1, y1, x2, y2) in enumerate(orig_canvas.get_selections()):selection_listbox.insert(tk.END, f"选区{i+1}: ({x1},{y1}) - ({x2},{y2})")orig_canvas.bind("<<SelectionCreated>>", lambda e: update_selection_listbox())def display_image(image):nonlocal orig_pil_image, current_displayed_imagecurrent_displayed_image = imageif image is None:returnif len(image.shape) == 2:pil_image = Image.fromarray(image)else:pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))orig_pil_image = pil_imageorig_canvas.show_image(pil_image)update_zoom_info()def update_zoom_info():if orig_canvas:zoom_percentage = int(orig_canvas.zoom * 100)zoom_info.config(text=f"缩放: {zoom_percentage}% | 鼠标滚轮缩放 | 中键拖拽")def load_image_file(entry_widget):nonlocal orig_pil_image, orig_image_cv, enhanced_image, current_displayed_imagefile_path = filedialog.askopenfilename(title="选择图像文件",filetypes=[("图像文件", "*.jpg *.jpeg *.png *.bmp *.tif")])if file_path:orig_canvas.clear_selections()selection_listbox.delete(0, tk.END)entry_widget.delete(0, tk.END)entry_widget.insert(0, file_path)def work():nonlocal orig_image_cv, enhanced_imageorig_image_cv = load_image(file_path)if orig_image_cv is not None:try:enhanced_image = extractor.enhance_image_advanced(orig_image_cv.copy())except Exception as e:print(f"图像优化失败: {str(e)}")enhanced_image = orig_image_cv.copy()def finish():if optimize_var.get():display_image(enhanced_image)else:display_image(orig_image_cv)status_bar.config(text="图像已加载,建议应用优化再进行选择")root.after(0, finish)else:root.after(0, lambda: status_bar.config(text="无法加载图像文件"))threading.Thread(target=work, daemon=True).start()def enhance_current_image(enhance=True):nonlocal enhanced_image, current_displayed_imageif orig_image_cv is None:returnstatus_bar.config(text="正在应用高级图像优化...")optimize_btn["state"] = "disabled"original_btn["state"] = "disabled"root.update_idletasks()def work():nonlocal enhanced_imagetry:enhanced_image = extractor.enhance_image_advanced(orig_image_cv.copy())except Exception as e:print(f"图像优化失败: {str(e)}")enhanced_image = orig_image_cv.copy()def finish():display_image(enhanced_image)status_bar.config(text="图像优化完成,现在可以精确选择签名区域")optimize_btn["state"] = "normal"original_btn["state"] = "normal"root.after(0, finish)threading.Thread(target=work, daemon=True).start()optimize_btn.config(command=lambda: enhance_current_image(True))original_btn.config(command=lambda: enhance_current_image(False))def reset_zoom_view():orig_canvas.reset_view()update_zoom_info()reset_zoom_btn.config(command=reset_zoom_view)def toggle_optimization():nonlocal use_enhanced_imageuse_enhanced_image = optimize_var.get()status_text = "启用高级优化" if use_enhanced_image else "禁用优化"optimize_status.config(text=f"({status_text})")if current_displayed_image is not None and orig_image_cv is not None:if use_enhanced_image and enhanced_image is not None:display_image(enhanced_image)else:display_image(orig_image_cv)optimize_cb.config(command=toggle_optimization)def extract_selected_signatures():input_path = input_entry.get()if not input_path:messagebox.showerror("错误", "请先选择输入图片")returnselections = orig_canvas.get_selections()if not selections:messagebox.showerror("错误", "未选择任何签名区域")returnoutput_dir = output_entry.get()if not output_dir:output_dir = extractor.output_settings['output_dir']status_bar.config(text="正在使用高级算法提取签名...")extract_btn["state"] = "disabled"root.update_idletasks()def work():use_enhanced = optimize_var.get()success, signature_paths, _ = extractor.extract_selected_signatures(input_path, selections, output_dir, use_enhanced_image=use_enhanced)def finish():extract_btn["state"] = "normal"if success and signature_paths:status_bar.config(text=f"提取完成!已保存 {len(signature_paths)} 个高质量签名")answer = messagebox.askyesno("提取完成",f"成功提取 {len(signature_paths)} 个签名,是否查看结果?")if answer:try:if os.name == 'nt':os.startfile(output_dir)elif os.name == 'posix':import subprocessimport sysopener = 'open' if sys.platform == 'darwin' else 'xdg-open'subprocess.Popen([opener, output_dir])except Exception as e:print(f"无法打开文件夹: {e}")status_bar.config(text=f"提取完成,但无法打开目录: {e}")else:status_bar.config(text="警告: 签名提取失败,请检查图像质量和选区位置")messagebox.showwarning("提取警告","签名提取失败。建议:\n""1. 启用高级优化功能\n""2. 确保选区包含完整签名\n""3. 检查原图像质量")root.after(0, finish)threading.Thread(target=work, daemon=True).start()extract_btn.config(command=extract_selected_signatures)def clear_selections():orig_canvas.clear_selections()selection_listbox.delete(0, tk.END)status_bar.config(text="已清除所有选区")clear_btn.config(command=clear_selections)def remove_selected():selected_indices = selection_listbox.curselection()if not selected_indices:returncurrent_sels = orig_canvas.get_selections()updated = [v for i,v in enumerate(current_sels) if i not in selected_indices]orig_canvas.selections = updatedorig_canvas.redraw()update_selection_listbox()status_bar.config(text=f"已删除{len(selected_indices)}个选区")remove_btn.config(command=remove_selected)def show_help():message = """高级签名提取工具2025增强版 - 快捷指南· 『应用优化』:CLAHE增强+白底优化+黑字加强
· 鼠标滚轮缩放, 中键拖动画布, 左键框选签名
· 推荐优化后再选区,选区略包含签名四周
· 内置多线程不卡,超顺滑体验!联系开发者/反馈建议 → [Your contact here]
"""messagebox.showinfo("使用指南", message)help_btn.config(command=show_help)def select_output_dir(entry_widget):dir_path = filedialog.askdirectory(title="选择输出目录")if dir_path:entry_widget.delete(0, tk.END)entry_widget.insert(0, dir_path)# 优化:智能防卡和黑屏补丁def window_event_refresh(event):orig_canvas.after(60, orig_canvas.redraw)root.bind("<FocusIn>", window_event_refresh)root.bind("<Configure>", lambda e: orig_canvas.after(30, orig_canvas.redraw))status_bar.config(text="高级签名提取工具已就绪 - 请加载图像")root.mainloop()if __name__ == '__main__':create_gui()