python 写的证件照片换背景工具,效果不理想,白边始终存在,有哪位大神给优化一下

360截图20260812132330223

 


import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk, ImageFilter, ImageChops, ImageOps
import os
import numpy as np


class IDPhotoBgChanger:
def __init__(self, root):
self.root = root
self.root.title("证件照精细化换背景 | 发丝去白边专业版")
# 固定窗口尺寸
WINDOW_W = 1200
WINDOW_H = 1050
self.root.geometry(f"{WINDOW_W}x{WINDOW_H}")
self.root.resizable(False, False)

# 窗口屏幕居中
screen_w = self.root.winfo_screenwidth()
screen_h = self.root.winfo_screenheight()
x_pos = (screen_w - WINDOW_W) // 2
y_pos = (screen_h - WINDOW_H) // 2
self.root.geometry(f"{WINDOW_W}x{WINDOW_H}+{x_pos}+{y_pos}")

self.root.configure(bg="#f0f4f9")

# 全局图像变量
self.original_img = None
self.processed_img = None
# 固定预览画布像素尺寸
self.preview_w = 430
self.preview_h = 560

# 可调参数绑定滑块
self.threshold_val = tk.IntVar(value=230)
self.blur_val = tk.IntVar(value=3)
self.erode_val = tk.IntVar(value=2)
self.edge_dark_val = tk.IntVar(value=30) # 边缘暗化强度

# 构建整体UI布局
self.build_ui()

def build_ui(self):
# ====================== 1. 顶部标题区域 ======================
title_frame = tk.Frame(self.root, bg="#f0f4f9")
title_frame.pack(fill="x", pady=12)
title_label = tk.Label(
title_frame,
text="证件照智能替换背景 · 发丝精细羽化 · 一键消除白边",
font=("微软雅黑", 20, "bold"),
bg="#f0f4f9",
fg="#165DFF"
)
title_label.pack()

# ====================== 2. 参数滑块调节区域(带数值显示) ======================
param_wrap = tk.LabelFrame(
self.root,
text="抠图参数调节",
font=("微软雅黑", 12, "bold"),
bg="#ffffff",
bd=2,
relief=tk.GROOVE
)
param_wrap.pack(fill="x", padx=40, pady=8)

# 使用网格布局,4行3列
for i in range(4):
param_wrap.grid_rowconfigure(i, weight=1)
for i in range(3):
param_wrap.grid_columnconfigure(i, weight=1)

# 第1行:背景阈值
tk.Label(param_wrap, text="背景亮度阈值:", bg="#ffffff", font=("微软雅黑", 10)).grid(row=0, column=0, padx=5,
pady=6, sticky="e")

threshold_frame = tk.Frame(param_wrap, bg="#ffffff")
threshold_frame.grid(row=0, column=1, padx=5, pady=6, sticky="w")

s1 = ttk.Scale(threshold_frame, from_=200, to=255, variable=self.threshold_val, orient=tk.HORIZONTAL,
length=180)
s1.pack(side=tk.LEFT)

self.threshold_label = tk.Label(threshold_frame, text="230", bg="#ffffff", font=("微软雅黑", 10, "bold"),
width=4, fg="#165DFF")
self.threshold_label.pack(side=tk.LEFT, padx=10)
self.threshold_val.trace('w', lambda *args: self.threshold_label.config(text=str(self.threshold_val.get())))

# 阈值说明
tk.Label(param_wrap, text="↑ 调高识别纯白背景,调低识别浅灰背景", bg="#ffffff", font=("微软雅黑", 8),
fg="#888888").grid(row=0, column=2, padx=5, pady=6, sticky="w")

# 第2行:羽化强度
tk.Label(param_wrap, text="发丝羽化强度:", bg="#ffffff", font=("微软雅黑", 10)).grid(row=1, column=0, padx=5,
pady=6, sticky="e")

blur_frame = tk.Frame(param_wrap, bg="#ffffff")
blur_frame.grid(row=1, column=1, padx=5, pady=6, sticky="w")

s2 = ttk.Scale(blur_frame, from_=0, to=6, variable=self.blur_val, orient=tk.HORIZONTAL, length=180)
s2.pack(side=tk.LEFT)

self.blur_label = tk.Label(blur_frame, text="3", bg="#ffffff", font=("微软雅黑", 10, "bold"), width=4,
fg="#165DFF")
self.blur_label.pack(side=tk.LEFT, padx=10)
self.blur_val.trace('w', lambda *args: self.blur_label.config(text=str(self.blur_val.get())))

tk.Label(param_wrap, text="↑ 数值越大发丝边缘过渡越柔和自然", bg="#ffffff", font=("微软雅黑", 8),
fg="#888888").grid(row=1, column=2, padx=5, pady=6, sticky="w")

# 第3行:白边消除力度
tk.Label(param_wrap, text="白边消除力度:", bg="#ffffff", font=("微软雅黑", 10)).grid(row=2, column=0, padx=5,
pady=6, sticky="e")

erode_frame = tk.Frame(param_wrap, bg="#ffffff")
erode_frame.grid(row=2, column=1, padx=5, pady=6, sticky="w")

s3 = ttk.Scale(erode_frame, from_=0, to=4, variable=self.erode_val, orient=tk.HORIZONTAL, length=180)
s3.pack(side=tk.LEFT)

self.erode_label = tk.Label(erode_frame, text="2", bg="#ffffff", font=("微软雅黑", 10, "bold"), width=4,
fg="#165DFF")
self.erode_label.pack(side=tk.LEFT, padx=10)
self.erode_val.trace('w', lambda *args: self.erode_label.config(text=str(self.erode_val.get())))

tk.Label(param_wrap, text="↑ 数值越大去除白边越彻底(可能轻微缩小人像)", bg="#ffffff", font=("微软雅黑", 8),
fg="#888888").grid(row=2, column=2, padx=5, pady=6, sticky="w")

# 第4行:边缘暗化(新增)
tk.Label(param_wrap, text="边缘暗化强度:", bg="#ffffff", font=("微软雅黑", 10)).grid(row=3, column=0, padx=5,
pady=6, sticky="e")

dark_frame = tk.Frame(param_wrap, bg="#ffffff")
dark_frame.grid(row=3, column=1, padx=5, pady=6, sticky="w")

s4 = ttk.Scale(dark_frame, from_=0, to=80, variable=self.edge_dark_val, orient=tk.HORIZONTAL, length=180)
s4.pack(side=tk.LEFT)

self.dark_label = tk.Label(dark_frame, text="30", bg="#ffffff", font=("微软雅黑", 10, "bold"), width=4,
fg="#165DFF")
self.dark_label.pack(side=tk.LEFT, padx=10)
self.edge_dark_val.trace('w', lambda *args: self.dark_label.config(text=str(self.edge_dark_val.get())))

tk.Label(param_wrap, text="↑ 数值越大边缘过渡越暗,减少白边反光(0-80)", bg="#ffffff", font=("微软雅黑", 8),
fg="#888888").grid(row=3, column=2, padx=5, pady=6, sticky="w")

# ====================== 3. 核心图片预览区域 ======================
preview_main = tk.Frame(self.root, bg="#f0f4f9")
preview_main.pack(pady=15)

# 左侧原图容器
left_box = tk.LabelFrame(
preview_main,
text="原始照片",
font=("微软雅黑", 12, "bold"),
bg="#ffffff",
bd=2,
relief=tk.GROOVE
)
left_box.grid(row=0, column=0, padx=35)
left_box.config(width=self.preview_w + 20, height=self.preview_h + 30)
left_box.pack_propagate(False)

self.origin_display = tk.Label(left_box, bg="#e9ecef")
self.origin_display.pack(expand=True, fill="both", padx=10, pady=10)

# 右侧处理后预览容器
right_box = tk.LabelFrame(
preview_main,
text="处理后预览(去白边+发丝优化)",
font=("微软雅黑", 12, "bold"),
bg="#ffffff",
bd=2,
relief=tk.GROOVE
)
right_box.grid(row=0, column=1, padx=35)
right_box.config(width=self.preview_w + 20, height=self.preview_h + 30)
right_box.pack_propagate(False)

self.result_display = tk.Label(right_box, bg="#e9ecef")
self.result_display.pack(expand=True, fill="both", padx=10, pady=10)

# ====================== 4. 底部固定按钮区域 ======================
btn_wrap = tk.LabelFrame(
self.root,
text="功能操作区",
font=("微软雅黑", 12, "bold"),
bg="#ffffff",
bd=2,
relief=tk.GROOVE
)
btn_wrap.pack(fill="x", padx=40, pady=8)

btn_config = {
"font": ("微软雅黑", 11),
"width": 14,
"height": 2,
"relief": tk.RAISED,
"bd": 1
}

tk.Button(btn_wrap, text="上传证件照片", bg="#3B82F6", fg="white", command=self.load_image, **btn_config).grid(
row=0, column=0, padx=5, pady=12)
tk.Button(btn_wrap, text="纯白色背景", bg="#FFFFFF", fg="#000000",
command=lambda: self.do_replace_bg((255, 255, 255)), **btn_config).grid(row=0, column=1, padx=5,
pady=12)
tk.Button(btn_wrap, text="身份证红底", bg="#DC2626", fg="white",
command=lambda: self.do_replace_bg((220, 0, 0)), **btn_config).grid(row=0, column=2, padx=5, pady=12)
tk.Button(btn_wrap, text="公考标准蓝底", bg="#2563EB", fg="white",
command=lambda: self.do_replace_bg((67, 142, 219)), **btn_config).grid(row=0, column=3, padx=5,
pady=12)
tk.Button(btn_wrap, text="保存成品图片", bg="#10B981", fg="white", command=self.save_output, **btn_config).grid(
row=0, column=4, padx=5, pady=12)

for col in range(5):
btn_wrap.grid_columnconfigure(col, weight=1)

# ====================== 最底部提示说明 ======================
tip_label = tk.Label(
self.root,
text="💡 使用技巧:①纯白背景用阈值230-240 ②发丝羽化3-5效果最佳 ③白边力度1-3去光晕 ④边缘暗化30-50减少反光",
font=("微软雅黑", 10),
bg="#f0f4f9",
fg="#475467"
)
tip_label.pack(pady=10)

def fit_picture_to_box(self, pil_img):
"""图片等比例缩放,自适应固定预览框居中"""
w, h = pil_img.size
max_w = self.preview_w - 30
max_h = self.preview_h - 30
scale_ratio = min(max_w / w, max_h / h)
new_w = int(w * scale_ratio)
new_h = int(h * scale_ratio)
try:
return pil_img.resize((new_w, new_h), Image.Resampling.LANCZOS)
except AttributeError:
return pil_img.resize((new_w, new_h), Image.LANCZOS)

def load_image(self):
"""上传图片"""
path = filedialog.askopenfilename(
title="选择证件照图片",
filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp"), ("所有文件", "*.*")]
)
if not path:
return
try:
self.original_img = Image.open(path).convert("RGBA")
self.processed_img = None
show_img = self.fit_picture_to_box(self.original_img)
tk_obj = ImageTk.PhotoImage(show_img)
self.origin_display.image = tk_obj
self.origin_display.config(image=tk_obj)
self.result_display.config(image="")
except Exception as e:
messagebox.showerror("图片读取失败", f"错误信息:{str(e)}")

def do_replace_bg(self, target_rgb):
"""核心抠图+去白边+发丝羽化算法(增强版)"""
if self.original_img is None:
messagebox.showwarning("操作提示", "请先点击【上传证件照片】选择图片!")
return
try:
thresh = self.threshold_val.get()
blur_r = self.blur_val.get()
erode_times = self.erode_val.get()
edge_dark = self.edge_dark_val.get()

img_w, img_h = self.original_img.size

# 转换为numpy数组便于处理
img_array = np.array(self.original_img)
r_channel = img_array[:, :, 0]
g_channel = img_array[:, :, 1]
b_channel = img_array[:, :, 2]
alpha_channel = img_array[:, :, 3] if img_array.shape[2] == 4 else np.ones((img_h, img_w)) * 255

# 1. 创建精细蒙版 - 使用多通道判断
# 计算亮度
gray = 0.299 * r_channel + 0.587 * g_channel + 0.114 * b_channel
mask_array = np.where(gray > thresh, 255, 0).astype(np.uint8)

# 2. 形态学处理 - 先膨胀后腐蚀,去除噪点
from scipy.ndimage import binary_erosion, binary_dilation, binary_closing
mask_bool = mask_array > 128

# 闭运算:先膨胀后腐蚀,填充小孔
if erode_times > 0:
# 多次腐蚀去除白边
for _ in range(erode_times):
mask_bool = binary_erosion(mask_bool, structure=np.ones((3, 3)))
# 膨胀恢复
for _ in range(erode_times // 2 + 1):
mask_bool = binary_dilation(mask_bool, structure=np.ones((3, 3)))

mask_array = mask_bool.astype(np.uint8) * 255

# 3. 转换为PIL图像进行高斯模糊
mask_pil = Image.fromarray(mask_array)

# 边缘羽化 - 多层高斯模糊
if blur_r > 0:
for i in range(2):
mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(radius=blur_r - i))

# 4. 转换为numpy进行边缘暗化处理
mask_final = np.array(mask_pil) / 255.0

# 5. 创建person_mask(人像区域)
person_mask = 1 - mask_final

# 6. 边缘暗化 - 针对边缘过渡区域
if edge_dark > 0:
# 检测边缘过渡区域(mask在0.1-0.9之间的区域)
edge_region = (mask_final > 0.05) & (mask_final < 0.95)
dark_factor = 1 - (edge_dark / 100) * np.abs(mask_final - 0.5) * 2
# 应用到RGB通道
for c in range(3):
img_array[:, :, c] = img_array[:, :, c] * person_mask + img_array[:, :,
c] * mask_final * dark_factor
else:
# 简单合成
for c in range(3):
img_array[:, :, c] = img_array[:, :, c] * person_mask + target_rgb[c] * mask_final

# 8. 合成新背景
result_img = Image.fromarray(img_array.astype(np.uint8))
self.processed_img = result_img.convert("RGB")

# 右侧预览渲染
preview_img = self.fit_picture_to_box(result_img)
preview_tk = ImageTk.PhotoImage(preview_img)
self.result_display.image = preview_tk
self.result_display.config(image=preview_tk)

except Exception as e:
messagebox.showerror("背景替换失败", f"异常:{str(e)}\n请尝试调整参数后重试")

def save_output(self):
"""保存处理完成的图片"""
if self.processed_img is None:
messagebox.showwarning("保存提示", "请先完成背景替换操作再保存!")
return
save_path = filedialog.asksaveasfilename(
defaultextension=".jpg",
filetypes=[("高质量JPG", "*.jpg"), ("PNG透明格式", "*.png")],
title="保存证件照成品"
)
if not save_path:
return
try:
self.processed_img.save(save_path, quality=98, subsampling=0)
messagebox.showinfo("保存成功", f"文件已保存至:{os.path.basename(save_path)}")
except Exception as e:
messagebox.showerror("保存失败", f"错误:{str(e)}")


if __name__ == "__main__":
root_window = tk.Tk()
app = IDPhotoBgChanger(root_window)
root_window.mainloop()
posted @ 2026-08-12 13:24  随风漂泊远方  阅读(0)  评论(0)    收藏  举报