[Windows] 【分享】手写签名提取工具
前情提要,昨天发布了一个将文字转成模拟手写的主题,有人要“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_gui
if __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 threading
try:
from ttkthemes import ThemedTk
except ImportError:
ThemedTk = None
from 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_extensions
def load_image(file_path):
if not os.path.exists(file_path):
return None
try:
image = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), 1)
if image is None:
raise IOError("无法读取图像文件")
return image
except Exception as e:
print(f"加载图像错误: {str(e)}")
return None
# --------- 核心图像处理 ----------
class SignatureExtractor:
def __init__(self):
self.output_settings = settings.OUTPUT
self.extraction_settings = settings.SIGNATURE_EXTRACTION
def 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, [], None
if not is_valid_image_file(input_path):
print(f"错误: 不支持的图像格式 - {input_path}")
return False, [], None
if not selections:
print("没有选择任何签名区域")
return False, [], None
if 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, [], None
if 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} 无效")
continue
try:
signature_img = self._extract_and_optimize_signature_advanced(region_image)
except Exception as e:
print(f"提取签名时出错: {e}")
continue
if 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_image
def 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 = image
lab = 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_channel
l_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_result
def _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 == 255
enhanced[background_indices] = enhanced[background_indices] * 1.2 + 30
enhanced = np.clip(enhanced, 0, 255).astype(np.uint8)
return enhanced
def _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 == 255
darkened[stroke_indices] = darkened[stroke_indices] * 0.6
darkened = np.clip(darkened, 0, 255).astype(np.uint8)
return darkened
def _extract_region(self, image, region):
x1, y1, x2, y2 = region
x_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 None
return 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 result
def _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 enhanced
def _multi_scale_edge_detection(self, gray_image):
scales = [1.0, 1.5, 2.0]
edges_list = []
for scale in scales:
sigma = scale
blurred = 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_edges
def _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 combined
def _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 = 50
final_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_mask
def _create_high_quality_transparent_image(self, original_image, mask):
height, width = mask.shape
transparent_image = np.zeros((height, width, 4), dtype=np.uint8)
for c in range(3):
transparent_image[:, :, c] = original_image[:, :, c]
transparent_image[:, :, 3] = mask
alpha_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_image
def _is_mostly_transparent(self, image):
if image is None or len(image.shape) < 3 or image.shape[2] < 4:
return True
alpha_channel = image[..., 3]
non_transparent_pixels = np.sum(alpha_channel > self.extraction_settings.get('min_alpha_value', 20))
total_pixels = alpha_channel.size
return non_transparent_pixels / total_pixels < 0.05
# --------- 优化后的缩放+拖拽画布组件 -----------
class ZoomableSelectionCanvas(tk.Canvas):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self.min_zoom = 0.15
self.max_zoom = 8
self.image_pil = None
self.image_tk = None
self.image_item = None
self.selections = []
self.dragging = False
self.start_imgxy = None
self.temp_rect = None
# --- 新增优化缓存 ---
self._pending_zoom_after = None
self._current_zoom_target = self.zoom
self._img_cache = {}
self._move_start = None
self.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_image
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self._img_cache.clear()
self.redraw()
def img2canvas(self, x, y):
return x * self.zoom + self.offset_x, y * self.zoom + self.offset_y
def canvas2img(self, cx, cy):
return (cx - self.offset_x) / self.zoom, (cy - self.offset_y) / self.zoom
def redraw(self):
self.delete("all")
if self.image_pil is None:
return
w, h = self.image_pil.size
disp_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_tk
self.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_target
self.redraw()
self._pending_zoom_after = None
if not self.image_pil: return
factor = 1.1 if event.delta > 0 else 0.9
self._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: return
self.dragging = True
img_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: return
if self.temp_rect:
self.delete(self.temp_rect)
x0, y0 = self.start_imgxy
x1, 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 = None
if not self.dragging or not self.image_pil: return
x0, y0 = self.start_imgxy
x1, 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: return
dx = event.x - self._move_start[0]
dy = event.y - self._move_start[1]
self.offset_x += dx
self.offset_y += dy
self._move_start = (event.x, event.y)
self.redraw()
def on_middle_up(self, event):
self.config(cursor="")
self._move_start = None
def get_selections(self):
return list(self.selections)
def clear_selections(self):
self.selections = []
self.redraw()
def reset_view(self):
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self.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, None
current_displayed_image = None
use_enhanced_image = True
extractor = 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_image
current_displayed_image = image
if image is None:
return
if len(image.shape) == 2:
pil_image = Image.fromarray(image)
else:
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
orig_pil_image = pil_image
orig_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_image
file_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_image
orig_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_image
if orig_image_cv is None:
return
status_bar.config(text="正在应用高级图像优化...")
optimize_btn["state"] = "disabled"
original_btn["state"] = "disabled"
root.update_idletasks()
def work():
nonlocal enhanced_image
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():
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_image
use_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("错误", "请先选择输入图片")
return
selections = orig_canvas.get_selections()
if not selections:
messagebox.showerror("错误", "未选择任何签名区域")
return
output_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 subprocess
import sys
opener = '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:
return
current_sels = orig_canvas.get_selections()
updated = [v for i,v in enumerate(current_sels) if i not in selected_indices]
orig_canvas.selections = updated
orig_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()
没有回复内容