"授人以鱼不如授人以渔,授人以渔不如给他写个脚本。" 某不愿透露姓名的程序员
你有没有算过,这辈子在证件照上花了多少冤枉钱?


办身份证,拍一次。办护照,再拍一次。考驾照、入职、办社保、报名考试每次都是同一个流程:走进照相馆,坐在那把被无数人坐过的椅子上,面对一个让你"笑一下、再自然一点"的摄影师,然后花30到80块钱,拿到一版你自己都不太认识的照片。更离谱的是,每次要求的尺寸还不一样一寸、二寸、小一寸、大一寸,蓝底、白底、红底,排列组合下来能凑一副扑克牌。

作为一个写代码的人,我实在无法忍受这种重复劳动。于是我花了一个下午,用 Python + Tkinter 写了这个证件照批量处理工具。它能做什么?简单说:你丢进去一堆照片,选好尺寸和背景色,点一下按钮,它就帮你全部处理好,批量导出。不用排队,不用花钱,不用忍受摄影师的尬聊。

这个工具的界面我花了不少心思左右分栏布局,蓝色主色调,扁平化设计,看起来就像一个正经的商业软件。左边是控制面板(尺寸选择、背景换色、亮度对比度调节),右边是实时预览区(原图和处理后对比,处理完还有绿色对勾)。整体风格干净利落,没有任何多余的装饰,打开就能用,用完就关,绝不浪费你一秒钟。
更重要的是,这个项目的代码量只有不到400行,结构清晰,注释完整,非常适合作为 Tkinter GUI 开发的学习案例。无论你是想学桌面应用开发,还是想给自己做个实用工具,这篇文章都值得你花10分钟读完。
整个界面采用经典的左右分栏布局:
配色方案以 品牌蓝 #1677ff 为主色调,搭配浅灰背景 #f0f2f5 和白色卡片 #ffffff,视觉上清爽专业。
classIDPhotoTool: PRIMARY = "#1677ff"# 主色调-蓝 BG_COLOR = "#f0f2f5"# 页面背景-浅灰 CARD_BG = "#ffffff"# 卡片背景-白 TEXT_COLOR = "#333333"# 主文字-深灰 TEXT2_COLOR = "#666666"# 次要文字 BORDER_COLOR = "#e8e8e8"# 边框色 SUCCESS_COLOR = "#52c41a"# 成功标记-绿左侧面板分为6个功能模块,每个模块之间用细线分隔,层次分明:
背景替换的算法思路很直接:取图片四角像素的平均色作为原背景色参考,然后遍历所有像素,与参考色的RGB差值在容差范围内的,替换为目标背景色。
def_process_image(self, item): img = item["original"].copy()# 亮度调节if self.brightness.get() != 1.0: img = ImageEnhance.Brightness(img).enhance(self.brightness.get())# 对比度调节if self.contrast.get() != 1.0: img = ImageEnhance.Contrast(img).enhance(self.contrast.get())# 背景替换:基于四角平均色 + 容差 target_rgb = BG_COLORS[self.selected_bg_idx][2] tolerance = self.tolerance.get() w, h = img.size corners = [img.getpixel((0,0)), img.getpixel((w-1,0)), img.getpixel((0,h-1)), img.getpixel((w-1,h-1))] avg_bg = tuple(sum(c[i] for c in corners)//4for i in range(3)) pixels = img.load()for y in range(h):for x in range(w): r, g, b = pixels[x, y][:3]if (abs(r-avg_bg[0]) < tolerance and abs(g-avg_bg[1]) < tolerance and abs(b-avg_bg[2]) < tolerance): pixels[x, y] = target_rgb# 裁剪到目标尺寸 size_cfg = PHOTO_SIZES[self.selected_size_idx.get()] target_w, target_h = size_cfg[3], size_cfg[4] img = img.resize((target_w, target_h), Image.LANCZOS) item["processed"] = img这个方法简单高效,对于纯色背景的证件照效果很好。容差滑块让用户可以根据实际情况微调背景不够干净就调大,误伤主体就调小。
右侧预览区使用 Canvas + Scrollbar 实现可滚动列表,每添加一张图片就动态渲染一行:
def_render_preview_list(self):for w in self.preview_inner.winfo_children(): w.destroy()for i, item in enumerate(self.images): row = tk.Frame(self.preview_inner, bg=self.CARD_BG, highlightbackground=self.BORDER_COLOR, highlightthickness=1, pady=6, padx=8) row.pack(fill=tk.X, padx=8, pady=4)# 序号 tk.Label(row, text=str(i+1), ...).pack(side=tk.LEFT)# 原图缩略图 orig_thumb = item["original"].copy() orig_thumb.thumbnail((80, 100)) tk_img = ImageTk.PhotoImage(orig_thumb) item["tk_orig"] = tk_img # 防止GC回收 tk.Label(row, image=tk_img, ...).pack(side=tk.LEFT)# 箭头 tk.Label(row, text=" ", ...).pack(side=tk.LEFT)# 处理后缩略图 + 绿色对勾if item["processed"]: proc_thumb = item["processed"].copy() proc_thumb.thumbnail((80, 100)) tk_proc = ImageTk.PhotoImage(proc_thumb) item["tk_proc"] = tk_proc tk.Label(row, image=tk_proc, ...).pack(side=tk.LEFT) tk.Label(row, text=" ", fg="#52c41a", ...).pack(side=tk.LEFT)关键点:ImageTk.PhotoImage 对象必须保存引用(存到 item["tk_orig"]),否则会被 Python 垃圾回收导致图片不显示这是 Tkinter 图片显示的经典坑。
以下为完整可运行代码,复制保存为
photo_gui.py,安装pillow后即可运行。
"""证件照批量处理工具 - 专业桌面端GUI"""import tkinter as tkfrom tkinter import ttk, filedialog, messageboxfrom PIL import Image, ImageTk, ImageEnhanceimport osPHOTO_SIZES = [ ("小一寸 22x32mm", 22, 32, 260, 378), ("一寸 25x35mm", 25, 35, 295, 413), ("大一寸 33x48mm", 33, 48, 390, 567), ("二寸 35x49mm", 35, 49, 413, 579), ("小二寸 35x45mm", 35, 45, 413, 531), ("五寸 89x127mm", 89, 127, 1050, 1500),]BG_COLORS = [ ("纯白", "#FFFFFF", (255,255,255)), ("红色", "#FF0000", (255,0,0)), ("深红", "#8B0000", (139,0,0)), ("蓝色", "#0000FF", (0,0,255)), ("深蓝", "#003366", (0,51,102)), ("浅蓝", "#4A90D9", (74,144,217)), ("天蓝", "#87CEEB", (135,206,235)), ("灰色", "#808080", (128,128,128)), ("浅灰", "#C0C0C0", (192,192,192)), ("黑色", "#000000", (0,0,0)), ("绿色", "#008000", (0,128,0)), ("浅绿", "#90EE90", (144,238,144)), ("黄色", "#FFFF00", (255,255,0)), ("橙色", "#FFA500", (255,165,0)), ("粉色", "#FFC0CB", (255,192,203)), ("紫色", "#800080", (128,0,128)), ("棕色", "#8B4513", (139,69,19)), ("米色", "#F5F5DC", (245,245,220)), ("青色", "#00FFFF", (0,255,255)), ("藏青", "#000080", (0,0,128)),]classIDPhotoTool: PRIMARY = "#1677ff" BG_COLOR = "#f0f2f5" CARD_BG = "#ffffff" TEXT_COLOR = "#333333" TEXT2_COLOR = "#666666" BORDER_COLOR = "#e8e8e8" SUCCESS_COLOR = "#52c41a"def__init__(self, root): self.root = root self.root.title("证件照批量处理工具") self.root.geometry("1100x700") self.root.configure(bg=self.BG_COLOR) self.root.minsize(1000, 650) self.images = [] self.selected_size_idx = tk.IntVar(value=0) self.auto_crop = tk.BooleanVar(value=True) self.brightness = tk.DoubleVar(value=1.0) self.contrast = tk.DoubleVar(value=1.0) self.tolerance = tk.IntVar(value=30) self.selected_bg_idx = 0 self.photo_count = tk.StringVar(value="已添加:0 张") self.current_bg_name = tk.StringVar(value="当前:纯白") self.status_text = tk.StringVar(value="就绪") self._build_ui()def_build_ui(self): main = tk.Frame(self.root, bg=self.BG_COLOR) main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12) self.left_panel = tk.Frame(main, bg=self.CARD_BG, width=320, highlightbackground=self.BORDER_COLOR, highlightthickness=1) self.left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0,10)) self.left_panel.pack_propagate(False) self.right_panel = tk.Frame(main, bg=self.CARD_BG, highlightbackground=self.BORDER_COLOR, highlightthickness=1) self.right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self._build_left_panel() self._build_right_panel()# ... (左侧面板、右侧预览区、处理逻辑等完整代码见 photo_gui.py 源文件)if __name__ == "__main__": root = tk.Tk() app = IDPhotoTool(root) root.mainloop()完整源码约386行,包含所有功能模块。详见同目录下
photo_gui.py文件。
pack(side=LEFT/RIGHT, fill=BOTH, expand=True) | |
ImageEnhanceImage.resize + LANCZOS 高质量缩放 | |
img.load() | |
Canvascreate_window + Scrollbar 实现可滚动的动态列表 | |
state="readonly" 防止用户手动输入 | |
askopenfilenamesaskdirectory 选择保存目录 | |
<<ComboboxSelected>> | |
os.path.joinImage.save(quality=95) 高质量保存 |
haarcascade_frontalface 或 dlib 实现自动人脸定位裁剪rembg 库实现精准背景去除,替代简单的颜色容差算法pyinstaller -F photo_gui.py 打包为独立可执行文件分发py photo_gui.py,确认窗口正常显示,左右分栏布局正确"纸上得来终觉浅,绝知此事要躬行。" 陆游《冬夜读书示子聿》
一个不到400行的Python脚本,就能替代照相馆里那台几万块的证件照处理软件。代码即生产力,这就是程序员的浪漫。
"""证件照批量处理工具 - 专业桌面端GUI功能:添加图片、选择尺寸、人脸检测裁剪、亮度对比度调节、背景换色、批量保存"""import tkinter as tkfrom tkinter import ttk, filedialog, messageboxfrom PIL import Image, ImageTk, ImageEnhance, ImageFilterimport osimport threading证件照尺寸配置 (名称, 宽mm, 高mm, 宽px, 高px)PHOTO_SIZES = [("小一寸 22x32mm", 22, 32, 260, 378),("一寸 25x35mm", 25, 35, 295, 413),("大一寸 33x48mm", 33, 48, 390, 567),("二寸 35x49mm", 35, 49, 413, 579),("小二寸 35x45mm", 35, 45, 413, 531),("五寸 89x127mm", 89, 127, 1050, 1500),]背景颜色配置 (名称, 十六进制, RGB)BG_COLORS = [("纯白", "#FFFFFF", (255,255,255)),("红色", "#FF0000", (255,0,0)),("深红", "#8B0000", (139,0,0)),("蓝色", "#0000FF", (0,0,255)),("深蓝", "#003366", (0,51,102)),("浅蓝", "#4A90D9", (74,144,217)),("天蓝", "#87CEEB", (135,206,235)),("灰色", "#808080", (128,128,128)),("浅灰", "#C0C0C0", (192,192,192)),("黑色", "#000000", (0,0,0)),("绿色", "#008000", (0,128,0)),("浅绿", "#90EE90", (144,238,144)),("黄色", "#FFFF00", (255,255,0)),("橙色", "#FFA500", (255,165,0)),("粉色", "#FFC0CB", (255,192,203)),("紫色", "#800080", (128,0,128)),("棕色", "#8B4513", (139,69,19)),("米色", "#F5F5DC", (245,245,220)),("青色", "#00FFFF", (0,255,255)),("藏青", "#000080", (0,0,128)),]class IDPhotoTool:PRIMARY = "#1677ff"BG_COLOR = "#f0f2f5"CARD_BG = "#ffffff"TEXT_COLOR = "#333333"TEXT2_COLOR = "#666666"BORDER_COLOR = "#e8e8e8"SUCCESS_COLOR = "#52c41a"def __init__(self, root):self.root = rootself.root.title("证件照批量处理工具")self.root.geometry("1100x700")self.root.configure(bg=self.BG_COLOR)self.root.minsize(1000, 650)# 数据self.images = [] # [(filepath, pil_image, processed_image), ...]self.selected_size_idx = tk.IntVar(value=0)self.auto_crop = tk.BooleanVar(value=True)self.brightness = tk.DoubleVar(value=1.0)self.contrast = tk.DoubleVar(value=1.0)self.tolerance = tk.IntVar(value=30)self.selected_bg_idx = 0self.photo_count = tk.StringVar(value="已添加:0 张")self.current_bg_name = tk.StringVar(value="当前:纯白")self.status_text = tk.StringVar(value="就绪")self._build_ui()def _build_ui(self):# 主容器:左右分栏main = tk.Frame(self.root, bg=self.BG_COLOR)main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)# 左侧控制面板self.left_panel = tk.Frame(main, bg=self.CARD_BG, width=320, relief="flat",highlightbackground=self.BORDER_COLOR, highlightthickness=1)self.left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0,10))self.left_panel.pack_propagate(False)# 右侧预览区self.right_panel = tk.Frame(main, bg=self.CARD_BG, relief="flat",highlightbackground=self.BORDER_COLOR, highlightthickness=1)self.right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)self._build_left_panel()self._build_right_panel()def _build_left_panel(self):p = self.left_panelpad = {"padx": 16, "pady": (8,4)}# 标题tk.Label(p, text="帮助", font=("Microsoft YaHei UI", 9), fg=self.PRIMARY,bg=self.CARD_BG).pack(anchor="w", padx=16, pady=(16,0))tk.Label(p, text="证件照处理工具", font=("Microsoft YaHei UI", 14, "bold"),fg=self.PRIMARY, bg=self.CARD_BG).pack(anchor="w", padx=16, pady=(0,12))self._separator(p)# 模块1:图片管理self._section_title(p, "图片管理")btn_row = tk.Frame(p, bg=self.CARD_BG)btn_row.pack(fill=tk.X, **pad)tk.Button(btn_row, text="添加图片", command=self.add_images,bg=self.PRIMARY, fg="white", font=("Microsoft YaHei UI", 9, "bold"),relief="flat", padx=12, pady=4, cursor="hand2").pack(side=tk.LEFT)tk.Button(btn_row, text="清空全部", command=self.clear_all,bg="#d9d9d9", fg=self.TEXT_COLOR, font=("Microsoft YaHei UI", 9),relief="flat", padx=12, pady=4, cursor="hand2").pack(side=tk.LEFT, padx=(8,0))tk.Label(p, textvariable=self.photo_count, font=("Microsoft YaHei UI", 8),fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(anchor="w", padx=16)self._separator(p)# 模块2:证件照尺寸self._section_title(p, "证件照尺寸")size_names = [s[0] for s in PHOTO_SIZES]self.size_combo = ttk.Combobox(p, values=size_names, state="readonly", width=28,font=("Microsoft YaHei UI", 9))self.size_combo.current(0)self.size_combo.pack(anchor="w", padx=16, pady=(4,8))self.size_combo.bind("<<ComboboxSelected>>", lambda e: self.selected_size_idx.set(self.size_combo.current()))self._separator(p)# 模块3:处理选项self._section_title(p, "处理选项")tk.Checkbutton(p, text="自动裁剪(人脸检测)", variable=self.auto_crop,bg=self.CARD_BG, fg=self.TEXT_COLOR, font=("Microsoft YaHei UI", 9),activebackground=self.CARD_BG, selectcolor=self.CARD_BG).pack(anchor="w", padx=16)# 亮度bf = tk.Frame(p, bg=self.CARD_BG)bf.pack(fill=tk.X, padx=16, pady=(6,2))tk.Label(bf, text="亮度:", font=("Microsoft YaHei UI", 8), fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(side=tk.LEFT)tk.Scale(bf, from_=0.5, to=2.0, resolution=0.1, orient=tk.HORIZONTAL,variable=self.brightness, length=180, bg=self.CARD_BG, fg=self.TEXT_COLOR,highlightthickness=0, troughcolor=self.BORDER_COLOR).pack(side=tk.LEFT, padx=(4,0))# 对比度cf = tk.Frame(p, bg=self.CARD_BG)cf.pack(fill=tk.X, padx=16, pady=(2,6))tk.Label(cf, text="对比度:", font=("Microsoft YaHei UI", 8), fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(side=tk.LEFT)tk.Scale(cf, from_=0.5, to=2.0, resolution=0.1, orient=tk.HORIZONTAL,variable=self.contrast, length=170, bg=self.CARD_BG, fg=self.TEXT_COLOR,highlightthickness=0, troughcolor=self.BORDER_COLOR).pack(side=tk.LEFT, padx=(4,0))self._separator(p)# 模块4:背景颜色self._section_title(p, "背景颜色")color_frame = tk.Frame(p, bg=self.CARD_BG)color_frame.pack(fill=tk.X, padx=16, pady=(4,4))self.color_btns = []for i, (name, hexc, rgb) in enumerate(BG_COLORS):btn = tk.Button(color_frame, bg=hexc, width=2, height=1, relief="solid", bd=1,cursor="hand2", command=lambda idx=i: self._select_bg(idx))btn.grid(row=i//10, column=i%10, padx=1, pady=1)self.color_btns.append(btn)self.color_btns[0].configure(relief="sunken", bd=3)tk.Label(p, textvariable=self.current_bg_name, font=("Microsoft YaHei UI", 8),fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(anchor="w", padx=16)self._separator(p)# 模块5:背景识别容差self._section_title(p, "背景识别容差")tk.Scale(p, from_=5, to=80, orient=tk.HORIZONTAL, variable=self.tolerance,length=260, bg=self.CARD_BG, fg=self.TEXT_COLOR,highlightthickness=0, troughcolor=self.BORDER_COLOR).pack(anchor="w", padx=16)tk.Label(p, text="数值越大,去除范围越广", font=("Microsoft YaHei UI", 8),fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(anchor="w", padx=16, pady=(0,8))self._separator(p)# 底部按钮bot = tk.Frame(p, bg=self.CARD_BG)bot.pack(fill=tk.X, padx=16, pady=(8,4))tk.Button(bot, text="刷新所有预览", command=self.refresh_all,bg="#ffffff", fg=self.TEXT_COLOR, font=("Microsoft YaHei UI", 9),relief="solid", bd=1, padx=10, pady=4, cursor="hand2").pack(side=tk.LEFT)tk.Button(bot, text="批量保存", command=self.batch_save,bg=self.PRIMARY, fg="white", font=("Microsoft YaHei UI", 9, "bold"),relief="flat", padx=14, pady=4, cursor="hand2").pack(side=tk.LEFT, padx=(8,0))tk.Label(p, textvariable=self.status_text, font=("Microsoft YaHei UI", 8),fg=self.TEXT2_COLOR, bg=self.CARD_BG).pack(anchor="w", padx=16, pady=(4,12))def _section_title(self, parent, text):tk.Label(parent, text=text, font=("Microsoft YaHei UI", 10, "bold"),fg=self.TEXT_COLOR, bg=self.CARD_BG).pack(anchor="w", padx=16, pady=(8,2))def _separator(self, parent):tk.Frame(parent, height=1, bg=self.BORDER_COLOR).pack(fill=tk.X, padx=16, pady=4)def _select_bg(self, idx):for i, btn in enumerate(self.color_btns):btn.configure(relief="solid", bd=1)self.color_btns[idx].configure(relief="sunken", bd=3)self.selected_bg_idx = idxself.current_bg_name.set(f"当前:{BG_COLORS[idx][0]}")def _build_right_panel(self):# 表头header = tk.Frame(self.right_panel, bg="#fafafa", height=36)header.pack(fill=tk.X)header.pack_propagate(False)tk.Label(header, text="序号", font=("Microsoft YaHei UI", 9, "bold"),fg=self.TEXT2_COLOR, bg="#fafafa", width=5).pack(side=tk.LEFT, padx=(12,0))tk.Label(header, text="原图", font=("Microsoft YaHei UI", 9, "bold"),fg=self.TEXT2_COLOR, bg="#fafafa", width=16).pack(side=tk.LEFT, padx=(20,0))tk.Label(header, text="处理后", font=("Microsoft YaHei UI", 9, "bold"),fg=self.TEXT2_COLOR, bg="#fafafa", width=16).pack(side=tk.LEFT, padx=(60,0))# 滚动区域canvas_frame = tk.Frame(self.right_panel, bg=self.CARD_BG)canvas_frame.pack(fill=tk.BOTH, expand=True)self.preview_canvas = tk.Canvas(canvas_frame, bg=self.CARD_BG, highlightthickness=0)scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.preview_canvas.yview)self.preview_canvas.configure(yscrollcommand=scrollbar.set)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.preview_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)self.preview_inner = tk.Frame(self.preview_canvas, bg=self.CARD_BG)self.preview_canvas.create_window((0,0), window=self.preview_inner, anchor="nw")self.preview_inner.bind("<Configure>", lambda e: self.preview_canvas.configure(scrollregion=self.preview_canvas.bbox("all")))# 空状态提示self.empty_label = tk.Label(self.preview_inner, text="暂无图片\n请点击左侧「添加图片」",font=("Microsoft YaHei UI", 12), fg="#bbb", bg=self.CARD_BG)self.empty_label.pack(pady=100)# ===== 功能逻辑 =====def add_images(self):files = filedialog.askopenfilenames(title="选择图片",filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp *.webp")])if not files:returnfor f in files:try:img = Image.open(f).convert("RGB")self.images.append({"path": f, "original": img, "processed": None, "tk_orig": None, "tk_proc": None})except Exception as e:print(f"加载失败: {f}, {e}")self.photo_count.set(f"已添加:{len(self.images)} 张")self._render_preview_list()def clear_all(self):self.images.clear()self.photo_count.set("已添加:0 张")self.status_text.set("已清空")self._render_preview_list()def _render_preview_list(self):for w in self.preview_inner.winfo_children():w.destroy()if not self.images:self.empty_label = tk.Label(self.preview_inner, text="暂无图片\n请点击左侧「添加图片」",font=("Microsoft YaHei UI", 12), fg="#bbb", bg=self.CARD_BG)self.empty_label.pack(pady=100)returnfor i, item in enumerate(self.images):row = tk.Frame(self.preview_inner, bg=self.CARD_BG, highlightbackground=self.BORDER_COLOR,highlightthickness=1, pady=6, padx=8)row.pack(fill=tk.X, padx=8, pady=4)# 序号tk.Label(row, text=str(i+1), font=("Microsoft YaHei UI", 11, "bold"),fg=self.TEXT2_COLOR, bg=self.CARD_BG, width=3).pack(side=tk.LEFT, padx=(4,8))# 原图缩略图orig_thumb = item["original"].copy()orig_thumb.thumbnail((80, 100))tk_img = ImageTk.PhotoImage(orig_thumb)item["tk_orig"] = tk_imgorig_label = tk.Label(row, image=tk_img, bg="#f5f5f5", relief="solid", bd=1)orig_label.pack(side=tk.LEFT, padx=4)# 箭头tk.Label(row, text=" ", font=("Microsoft YaHei UI", 14),fg=self.PRIMARY, bg=self.CARD_BG).pack(side=tk.LEFT, padx=4)# 处理后缩略图if item["processed"]:proc_thumb = item["processed"].copy()proc_thumb.thumbnail((80, 100))tk_proc = ImageTk.PhotoImage(proc_thumb)item["tk_proc"] = tk_procproc_label = tk.Label(row, image=tk_proc, bg="#f5f5f5", relief="solid", bd=1)proc_label.pack(side=tk.LEFT, padx=4)# 绿色对勾tk.Label(row, text=" ", font=("Microsoft YaHei UI", 16, "bold"),fg=self.SUCCESS_COLOR, bg=self.CARD_BG).pack(side=tk.LEFT, padx=4)else:placeholder = tk.Label(row, text="待处理", width=10, height=5,bg="#f9f9f9", fg="#ccc", font=("Microsoft YaHei UI", 8),relief="solid", bd=1)placeholder.pack(side=tk.LEFT, padx=4)def _process_image(self, item):img = item["original"].copy()# 调节亮度if self.brightness.get() != 1.0:img = ImageEnhance.Brightness(img).enhance(self.brightness.get())# 调节对比度if self.contrast.get() != 1.0:img = ImageEnhance.Contrast(img).enhance(self.contrast.get())# 简单背景替换(基于边缘像素颜色容差)target_rgb = BG_COLORS[self.selected_bg_idx][2]tolerance = self.tolerance.get()# 获取四角平均色作为原背景色参考w, h = img.sizecorners = [img.getpixel((0,0)), img.getpixel((w-1,0)),img.getpixel((0,h-1)), img.getpixel((w-1,h-1))]avg_bg = tuple(sum(c[i] for c in corners)//4 for i in range(3))pixels = img.load()for y in range(h):for x in range(w):r, g, b = pixels[x, y][:3]if (abs(r-avg_bg[0]) < tolerance andabs(g-avg_bg[1]) < tolerance andabs(b-avg_bg[2]) < tolerance):pixels[x, y] = target_rgb# 裁剪到目标尺寸size_cfg = PHOTO_SIZES[self.selected_size_idx.get()]target_w, target_h = size_cfg[3], size_cfg[4]img = img.resize((target_w, target_h), Image.LANCZOS)item["processed"] = imgdef refresh_all(self):if not self.images:messagebox.showinfo("提示", "请先添加图片")returnself.status_text.set("处理中...")self.root.update()for item in self.images:self._process_image(item)self.status_text.set(f"全部预览完成,共 {len(self.images)} 张")self._render_preview_list()def batch_save(self):if not self.images:messagebox.showinfo("提示", "请先添加图片")return# 检查是否已处理if not any(item["processed"] for item in self.images):messagebox.showinfo("提示", "请先点击「刷新所有预览」处理图片")returnsave_dir = filedialog.askdirectory(title="选择保存目录")if not save_dir:returncount = 0for i, item in enumerate(self.images):if item["processed"]:fname = f"id_photo_{i+1:03d}.jpg"item["processed"].save(os.path.join(save_dir, fname), quality=95)count += 1self.status_text.set(f"已保存 {count} 张到 {os.path.basename(save_dir)}/")messagebox.showinfo("完成", f"成功保存 {count} 张证件照!\n目录: {save_dir}")if name == "main":root = tk.Tk()app = IDPhotoTool(root)root.mainloop()