智能图片处理系统——证件照背景替换
利用图割理论和高斯混合模型(GMM),在仅知道大概区域(矩形框)的情况下,通过颜色统计精准分离前景和背景。闭运算 (MORPH_CLOSE),开运算 (MORPH_OPEN),高斯模糊 (GaussianBlur),轮廓检测 (findContours),Pillow (PIL) - 图像合成与格式转换,图像混合 (alpha_composite):利用 Alpha 通道将抠好的人像图层与纯色背景
证件照背景替换
1.导入与全局变量
import tkinter as tk
导入 Python 的标准 GUI 库 Tkinter,并给它取别名 tk,便于后续使用如 tk.Tk()、tk.Button() 等控件。
作用:创建主窗口、按钮、标签、菜单等 GUI 元件,处理事件循环。from tkinter import filedialog,messagebox从 tkinter 模块中专门导入两个子模块:filedialog:提供打开文件、保存文件等对话框,常用于让用户选择图片、视频等资源。messagebox:提供简单的消息对话框,用于提示、确认和错误信息。 作用:实现文件选择和用户提示等交互功能。from PIL import Image, ImageTk
从 Pillow 库导入 Image 和 ImageTk:Image:用于图像的打开、处理、保存等操作(PIL 的核心对象)。ImageTk:将 PIL 图像转换为 Tkinter 可以显示的图像对象(如 PhotoImage)。 作用:在 Tkinter
界面中显示和处理图像,结合 OpenCV 进行更丰富的图像处理。import cv2
导入 OpenCV 库(通常以 cv2 为命名空间)。
作用:提供强大的计算机视觉和图像处理功能,如读取、解码、转换、滤镜、边缘检测、对象检测等。import numpy as np
导入 NumPy 库并给出别名 np。
作用:在图像处理中广泛使用,用于处理数组、矩阵、图像数据的数值运算,OpenCV的大部分接口也返回/接受 NumPy 数组。import os
导入 os 模块,用于跨平台的文件路径和操作系统相关功能(如路径拼接、检查文件存在性、环境变量等)。
作用:与文件系统交互,组合资源路径,读取/写入文件等。
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
import cv2
import numpy as np
import os
定义全局变量 original_image,初始化为空,用于存储加载的原始 PIL 图片对象。 定义全局变量
processed_image,用于存储当前更换背景色后的最终图片对象。 定义全局变量
transparent_image,用于存储经过算法抠图后、去除背景的透明图片对象(RGBA 模式)。
# --- 全局变量 ---
original_image = None # PIL 格式原图
processed_image = None # PIL 格式处理后图片
transparent_image = None # PIL 格式透明背景图(RGBA)
2.start_bg_tool 函数(抠图逻辑)
它的主要流程是:让用户选择图片 -> 弹出加载窗口 -> 使用 OpenCV 的 GrabCut 算法进行智能抠图 -> 对抠图结果进行优化(修补、去噪、羽化) -> 生成透明背景图 -> 调用界面窗口显示结果。
定义名为 start_bg_tool 的函数,文档字符串说明这是主界面调用的入口。
声明在函数内部将要修改这三个全局变量,否则 Python会将它们视为局部变量。
def start_bg_tool():
"""主界面调用的入口函数"""
global original_image, processed_image, transparent_image
1. 让用户选择图片
打开一个文件选择对话框,标题为“选择证件照”,限制只能选择常见图片格式或所有文件,并将选中的路径赋值给 file_path。
如果用户取消了选择(路径为空字符串),则直接退出函数,不执行后续操作。
file_path = filedialog.askopenfilename(
title="选择证件照",
filetypes=[("图片文件", "*.jpg *.jpeg *.png"), ("所有文件", "*.*")]
)
if not file_path:
return
2. 弹出处理提示窗口
创建一个新的顶级窗口(浮窗),用于显示处理状态。
设置该窗口的标题为“提示”,并设定窗口大小为 350x120 像素。
设置窗口属性-topmost 为 True,使该窗口始终显示在最前端,防止用户点击其他地方导致处理窗口被遮挡。
在处理窗口中创建一个标签,显示提示文字,并设置垂直内边距为 20。
processing_window = tk.Toplevel()
processing_window.title("提示")
processing_window.geometry("350x120")
# 设置窗口置顶,防止用户误操作
processing_window.attributes('-topmost', True)
tk.Label(processing_window, text="正在智能抠图与边缘精修...", font=("Arial", 12)).pack(pady=20)
processing_window.update() # 强制刷新界面
3.读取图片
开始异常捕获块。如果后续代码出错(如文件损坏),程序不会崩溃,而是跳转到 except 块。
检查文件路径是否真实存在,如果不存在则手动抛出异常。 以二进制流形式读取文件内容到 numpy
数组。关键点:这种方法支持包含中文的文件路径,解决了 cv2.imread 对中文不友好的问题。 将 numpy 数组解码为 OpenCV
图像格式(img_cv),以彩色模式读取。 检查解码结果是否为空(None),如果为空说明文件可能不是有效图片,抛出异常。
获取图像的高度和宽度。
try:
# --- 读取图片 (支持中文路径) ---
if not os.path.exists(file_path):
raise Exception("文件不存在")
img_array = np.fromfile(file_path, dtype=np.uint8)
img_cv = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if img_cv is None:
raise Exception("无法解码图片,文件可能已损坏")
height, width = img_cv.shape[:2]
4.GrabCut 算法
检查解码结果是否为空(None),如果为空说明文件可能不是有效图片,抛出异常。 获取图像的高度和宽度。 自动计算一个矩形框,距离图片边缘
10%。假设人物在图片中间,这个框作为算法的初始参考。 初始化 GrabCut 算法所需的临时变量:掩码、背景模型和前景模型数组。 执行
GrabCut 算法。输入图像、掩码、矩形框等,迭代 10 次,利用矩形框模式进行初始化计算。
处理算法输出的掩码,将背景类(0和2)转为0(黑),前景类(1和3)转为1(白),得到一个黑白二值图。
进行形态学闭运算(先膨胀后腐蚀),用于填补前景物体内部可能出现的黑洞(如衣服纹理)。
进行形态学开运算(先腐蚀后膨胀),用于去除边缘细小的白色噪点。 对掩码进行高斯模糊,使锐利的边缘产生灰度渐变,实现羽化效果,让合成更自然。
将原图与掩码相乘。背景部分(掩码0)变黑,前景部分(掩码1)保留。利用广播机制将二维掩码扩展到三维。
# 1. 自动计算 ROI (感兴趣区域)
margin_x = int(width * 0.1)
margin_y = int(height * 0.1)
rect = (margin_x, margin_y, width - margin_x, height - margin_y)
# 2. 初始化 GrabCut 模型
mask = np.zeros(img_cv.shape[:2], np.uint8)
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
# 3. 运行 GrabCut (迭代次数 15)
cv2.grabCut(img_cv, mask, rect, bgd_model, fgd_model, 10, cv2.GC_INIT_WITH_RECT)
# 4. 修改掩码:将“可能是背景”(0,2) 设为 0,将“前景”(1,3) 设为 1
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
# 5. 缺陷修复:闭运算(填补前景内部空洞)
kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask2 = cv2.morphologyEx(mask2, cv2.MORPH_CLOSE, kernel_close, iterations=2)
# 6. 边缘形态学处理 (去噪)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
mask2 = cv2.morphologyEx(mask2, cv2.MORPH_OPEN, kernel, iterations=1)
# 7. 边缘羽化 (使边缘平滑)
mask2 = cv2.GaussianBlur(mask2, (3, 3), 0)
# 8. 应用掩码提取前景
foreground = img_cv * mask2[:, :, np.newaxis]
5.转换为 PIL 格式
将 OpenCV 默认的 BGR 颜色顺序转换为 PIL 需要的 RGB 顺序。 将 numpy 数组转换为 PIL 图像对象。
将处理后的浮点掩码转为 0-255 的灰度图,作为透明度通道的基础。 查找掩码中的所有轮廓。创建一个全黑的新掩码用于存放清理后的结果。
如果存在轮廓,找出面积最大的那个(通常是人物主体),将其填充到新掩码中。这步操作切掉了所有非主体的杂质。最后再次高斯模糊保持边缘羽化。
将清理后的掩码作为 Alpha 通道添加到 PIL 图片中,生成最终的透明背景图(RGBA)。 将生成的图片赋值给全局变量,供后续使用。
处理完成,销毁“正在处理…”的提示窗口。
# 1. BGR 转 RGB
foreground_rgb = cv2.cvtColor(foreground, cv2.COLOR_BGR2RGB)
# 2. 转为 PIL Image
pil_img = Image.fromarray(foreground_rgb)
# 3. 生成 Alpha 通道
alpha_mask = Image.fromarray((mask2 * 255).astype(np.uint8))
# 4. 计算总轮廓以消除内部噪点
contours, _ = cv2.findContours(mask2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask_clean = np.zeros_like(mask2)
if contours:
max_contour = max(contours, key=cv2.contourArea)
cv2.drawContours(mask_clean, [max_contour], -1, 255, -1)
mask_clean = cv2.GaussianBlur(mask_clean, (3, 3), 0)
# 合并 RGB 和 Alpha 通道
pil_img.putalpha(Image.fromarray(mask_clean))
transparent_image = pil_img
original_image = pil_img
processed_image = pil_img.copy()
# 关闭处理窗口
processing_window.destroy()
6.异常处理
异常处理代码。如果出错,销毁提示窗口,弹出错误对话框,并在控制台打印详细错误堆栈,然后退出函数。
except Exception as e:
processing_window.destroy()
messagebox.showerror("处理失败", f"抠图出错:\n{str(e)}")
import traceback
traceback.print_exc()
return
7. 打开操作窗口
抠图成功后,调用 open_tool_window 函数,打开操作界面,并将透明图传进去。
open_tool_window(transparent_image)
3.open_tool_window 函数(界面与交互)
该函数负责构建用户的操作界面。界面分为左(原图)、中(按钮)、右(预览)三部分。它实现了将透明图与不同颜色的背景合成,并支持保存结果。
定义界面显示函数,声明使用全局变量 processed_image。创建新的顶级窗口。
设置标题和大小。
def open_tool_window(img_no_bg):
"""显示操作窗口 - 原图垂直偏移版"""
global processed_image
top = tk.Toplevel()
top.title("证件照背景处理工具 (OpenCV优化版)")
top.geometry("800x500")
1.创建透明棋盘格背景
定义内部函数,用于生成灰白相间的棋盘格图像,这是图像处理中表示“透明”的通用方式,方便用户看清抠图边缘。
def create_transparent_grid(width, height):
img = Image.new("RGB", (width, height), "#e0e0e0")
pixels = img.load()
for y in range(height):
for x in range(width):
if (x // 20 + y // 20) % 2 == 0:
pixels[x, y] = (200, 200, 200)
return img
4.页面布局
左侧:原图显示
创建左侧容器 Frame,设置背景色,并将其放置在窗口左边。
创建一个空白的标签作为占位符,通过设置顶部内边距将下方的内容“挤”下去,实现垂直位置的调整。 添加“原图 (已去背景)”的标题标签。
中间:控制按钮
创建中间容器 Frame,放置在窗口中间,用于放置操作按钮。
右侧:效果图显示
创建右侧容器 Frame,放置在窗口右边,用于显示换底后的效果图。
图片显示参数
定义图片显示的固定最大高度和宽度,确保图片在不同分辨率下都能良好显示且保持对齐。
左侧显示逻辑
复制透明图,并使用 thumbnail 方法按比例缩放图片以适应显示区域。
生成与缩放后图片等大的棋盘格背景,并将透明图粘贴上去。第三个参数 display_img_left 是掩码,用于实现透明叠加。
将合成后的图片转换为 Tkinter 可显示的对象,创建 Label 显示它。label_original.image = photo_original 防止图片被垃圾回收。
# 左侧:原图显示
frame_left = tk.Frame(top, bg="#e0e0e0", width=280)
frame_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 【布局修改】在上方增加一个垂直占位符,使原图位置下移
spacer_label = tk.Label(frame_left, text="", bg="#e0e0e0")
spacer_label.pack(pady=(60, 0)) # 调整这个 60 可以改变下移距离
tk.Label(frame_left, text="原图 (已去背景)", bg="#e0e0e0", font=("Arial", 10, "bold")).pack(pady=5)
# 中间:控制按钮
frame_mid = tk.Frame(top, bg="#f0f0f0", width=120)
frame_mid.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=10)
# 右侧:效果图显示
frame_right = tk.Frame(top, bg="#e0e0e0", width=280)
frame_right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
tk.Label(frame_right, text="换底预览", bg="#e0e0e0", font=("Arial", 10, "bold")).pack(pady=5)
# --- 图片显示参数 (确保Y轴对齐) ---
FIXED_DISPLAY_HEIGHT = 450
MAX_DISPLAY_WIDTH = 260
# --- 左侧显示逻辑 ---
display_img_left = img_no_bg.copy()
display_img_left.thumbnail((MAX_DISPLAY_WIDTH, FIXED_DISPLAY_HEIGHT))
grid_bg = create_transparent_grid(display_img_left.width, display_img_left.height)
grid_bg.paste(display_img_left, (0, 0), display_img_left)
photo_original = ImageTk.PhotoImage(grid_bg)
label_original = tk.Label(frame_left, image=photo_original, bg="#e0e0e0")
label_original.image = photo_original
label_original.pack()
5.换背景色
定义换色函数。创建纯色背景层,如果抠图是 RGBA 模式,使用 alpha_composite 将背景层和人像层混合,利用 Alpha通道实现完美的背景合成。
保存合成结果到全局变量,并调用 update_display 更新右侧预览。
def change_color(color_rgb):
global processed_image
width, height = img_no_bg.size
color_bg = Image.new("RGB", (width, height), color_rgb)
if img_no_bg.mode == 'RGBA':
color_bg_rgba = color_bg.convert("RGBA")
final_img = Image.alpha_composite(color_bg_rgba, img_no_bg)
else:
final_img = img_no_bg
processed_image = final_img
update_display(final_img)
6.显示新图像
定义更新显示的函数。为了预览效果,将 RGBA 图片合成为白底 RGB 图片(防止预览窗口看起来是透明黑底)。
缩放预览图,转换为 Tkinter 对象,并更新右侧 Label 的显示内容。
def update_display(img_pil):
if img_pil.mode == 'RGBA':
display_img = Image.new("RGB", img_pil.size, (255, 255, 255))
display_img.paste(img_pil, mask=img_pil.split()[3])
else:
display_img = img_pil
display_img.thumbnail((MAX_DISPLAY_WIDTH, FIXED_DISPLAY_HEIGHT))
photo_new = ImageTk.PhotoImage(display_img)
label_result.config(image=photo_new)
label_result.image = photo_new
7.保存图像
定义保存函数。如果没有处理好的图片,弹出警告。
弹出保存文件对话框,默认扩展名为 .jpg。
如果用户确认了保存路径: 如果是 JPG格式(不支持透明),先将背景合成为白色,然后转换为 RGB 模式保存,质量设为 95。 这样可以避免保存出来的照片背景变黑。
如果是 PNG格式,直接保存(保留透明度)。捕获保存过程中的异常并提示。
def save_img():
global processed_image
if not processed_image:
messagebox.showwarning("提示", "没有可保存的图片")
return
path = filedialog.asksaveasfilename(
defaultextension=".jpg",
filetypes=[("JPEG 图片", "*.jpg"), ("PNG 透明图", "*.png")]
)
if path:
try:
ext = os.path.splitext(path)[1].lower()
if ext in ['.jpg', '.jpeg']:
if processed_image.mode == 'RGBA':
white_bg = Image.new("RGB", processed_image.size, (255, 255, 255))
white_bg.paste(processed_image, mask=processed_image.split()[3])
save_img_obj = white_bg
else:
save_img_obj = processed_image.convert("RGB")
save_img_obj.save(path, quality=95)
else:
processed_image.save(path)
messagebox.showinfo("成功", "图片已保存!")
except Exception as e:
messagebox.showerror("保存失败", f"错误:{str(e)}")
8.定义按钮
定义标准的证件照背景色 RGB 值。
btn_style:定义按钮的统一样式字典。
创建“换成蓝\红\白底”按钮,点击时调用change_color 并传入蓝色 RGB 值。tk.Frame:添加一个空的 Frame作为占位,在上方按钮和保存按钮之间留出空隙。
创建“保存结果”按钮。
color_blue = (67, 142, 219)
color_red = (220, 50, 47)
color_white = (255, 255, 255)
btn_style = {"width": 12, "height": 2, "font": ("Arial", 10)}
tk.Button(frame_mid, text="换成蓝底", bg="#add8e6", **btn_style,
command=lambda: change_color(color_blue)).pack(pady=15)
tk.Button(frame_mid, text="换成红底", bg="#ffcccb", **btn_style,
command=lambda: change_color(color_red)).pack(pady=15)
tk.Button(frame_mid, text="换成白底", bg="white", relief="solid", **btn_style,
command=lambda: change_color(color_white)).pack(pady=15)
tk.Frame(frame_mid, height=20).pack() # 占位符
tk.Button(frame_mid, text="保存结果", bg="#dddddd", **btn_style,
command=save_img).pack(pady=30)
9.右侧初始化
初始化右侧预览 Label。程序启动时默认执行一次 change_color(color_white),让窗口一打开就显示白底效果图。
label_result = tk.Label(frame_right, bg="#e0e0e0")
label_result.pack(expand=True)
change_color(color_white)
10.主程序入口
程序运行的起始点。它创建了一个隐藏的主窗口,并直接启动抠图工具的主逻辑。
判断当前脚本是否作为主程序运行(而不是被作为模块导入)。
root = tk.Tk,root.withdraw:创建 Tkinter根窗口,并立即调用 withdraw 将其隐藏。因为我们的界面是通过 Toplevel 弹出的,不需要这个空的主窗口。start_bg_tool:调用抠图工具的主入口函数,开始运行程序。root.mainloop:进入 Tkinter的主事件循环,保持程序运行,等待用户操作。
if __name__ == "__main__":
root = tk.Tk()
root.withdraw() # 隐藏主窗口,作为模块使用时
start_bg_tool()
root.mainloop()
11.运行结果



12.总代码
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
import cv2
import numpy as np
import os
# --- 全局变量 ---
original_image = None # PIL 格式原图
processed_image = None # PIL 格式处理后图片
transparent_image = None # PIL 格式透明背景图(RGBA)
def start_bg_tool():
"""主界面调用的入口函数"""
global original_image, processed_image, transparent_image
# 1. 让用户选择图片
file_path = filedialog.askopenfilename(
title="选择证件照",
filetypes=[("图片文件", "*.jpg *.jpeg *.png"), ("所有文件", "*.*")]
)
if not file_path:
return
# 2. 弹出处理提示窗口
processing_window = tk.Toplevel()
processing_window.title("提示")
processing_window.geometry("350x120")
# 设置窗口置顶,防止用户误操作
processing_window.attributes('-topmost', True)
tk.Label(processing_window, text="正在智能抠图与边缘精修...", font=("Arial", 12)).pack(pady=20)
processing_window.update() # 强制刷新界面
try:
# --- 读取图片 (支持中文路径) ---
if not os.path.exists(file_path):
raise Exception("文件不存在")
img_array = np.fromfile(file_path, dtype=np.uint8)
img_cv = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
if img_cv is None:
raise Exception("无法解码图片,文件可能已损坏")
height, width = img_cv.shape[:2]
# --- 核心:改进的 GrabCut 算法 ---
# 1. 自动计算 ROI (感兴趣区域)
margin_x = int(width * 0.1)
margin_y = int(height * 0.1)
rect = (margin_x, margin_y, width - margin_x, height - margin_y)
# 2. 初始化 GrabCut 模型
mask = np.zeros(img_cv.shape[:2], np.uint8)
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
# 3. 运行 GrabCut (迭代次数 15)
cv2.grabCut(img_cv, mask, rect, bgd_model, fgd_model, 10, cv2.GC_INIT_WITH_RECT)
# 4. 修改掩码:将“可能是背景”(0,2) 设为 0,将“前景”(1,3) 设为 1
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')
# 5. 缺陷修复:闭运算(填补前景内部空洞)
kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask2 = cv2.morphologyEx(mask2, cv2.MORPH_CLOSE, kernel_close, iterations=2)
# 6. 边缘形态学处理 (去噪)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
mask2 = cv2.morphologyEx(mask2, cv2.MORPH_OPEN, kernel, iterations=1)
# 7. 边缘羽化 (使边缘平滑)
mask2 = cv2.GaussianBlur(mask2, (3, 3), 0)
# 8. 应用掩码提取前景
foreground = img_cv * mask2[:, :, np.newaxis]
# --- 转换为 PIL 格式 ---
# 1. BGR 转 RGB
foreground_rgb = cv2.cvtColor(foreground, cv2.COLOR_BGR2RGB)
# 2. 转为 PIL Image
pil_img = Image.fromarray(foreground_rgb)
# 3. 生成 Alpha 通道
alpha_mask = Image.fromarray((mask2 * 255).astype(np.uint8))
# 4. 计算总轮廓以消除内部噪点
contours, _ = cv2.findContours(mask2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask_clean = np.zeros_like(mask2)
if contours:
max_contour = max(contours, key=cv2.contourArea)
cv2.drawContours(mask_clean, [max_contour], -1, 255, -1)
mask_clean = cv2.GaussianBlur(mask_clean, (3, 3), 0)
# 合并 RGB 和 Alpha 通道
pil_img.putalpha(Image.fromarray(mask_clean))
transparent_image = pil_img
original_image = pil_img
processed_image = pil_img.copy()
# 关闭处理窗口
processing_window.destroy()
except Exception as e:
processing_window.destroy()
messagebox.showerror("处理失败", f"抠图出错:\n{str(e)}")
import traceback
traceback.print_exc()
return
# 3. 打开操作窗口
open_tool_window(transparent_image)
def open_tool_window(img_no_bg):
"""显示操作窗口 - 原图垂直偏移版"""
global processed_image
top = tk.Toplevel()
top.title("证件照背景处理工具 (OpenCV优化版)")
top.geometry("800x500")
# --- 辅助函数:创建透明棋盘格背景 ---
def create_transparent_grid(width, height):
img = Image.new("RGB", (width, height), "#e0e0e0")
pixels = img.load()
for y in range(height):
for x in range(width):
if (x // 20 + y // 20) % 2 == 0:
pixels[x, y] = (200, 200, 200)
return img
# --- 布局 ---
# 左侧:原图显示
frame_left = tk.Frame(top, bg="#e0e0e0", width=280)
frame_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 【布局修改】在上方增加一个垂直占位符,使原图位置下移
spacer_label = tk.Label(frame_left, text="", bg="#e0e0e0")
spacer_label.pack(pady=(60, 0)) # 调整这个 60 可以改变下移距离
tk.Label(frame_left, text="原图 (已去背景)", bg="#e0e0e0", font=("Arial", 10, "bold")).pack(pady=5)
# 中间:控制按钮
frame_mid = tk.Frame(top, bg="#f0f0f0", width=120)
frame_mid.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=10)
# 右侧:效果图显示
frame_right = tk.Frame(top, bg="#e0e0e0", width=280)
frame_right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
tk.Label(frame_right, text="换底预览", bg="#e0e0e0", font=("Arial", 10, "bold")).pack(pady=5)
# --- 图片显示参数 (确保Y轴对齐) ---
FIXED_DISPLAY_HEIGHT = 450
MAX_DISPLAY_WIDTH = 260
# --- 左侧显示逻辑 ---
display_img_left = img_no_bg.copy()
display_img_left.thumbnail((MAX_DISPLAY_WIDTH, FIXED_DISPLAY_HEIGHT))
grid_bg = create_transparent_grid(display_img_left.width, display_img_left.height)
grid_bg.paste(display_img_left, (0, 0), display_img_left)
photo_original = ImageTk.PhotoImage(grid_bg)
label_original = tk.Label(frame_left, image=photo_original, bg="#e0e0e0")
label_original.image = photo_original
label_original.pack()
# --- 核心功能:换色 ---
def change_color(color_rgb):
global processed_image
width, height = img_no_bg.size
color_bg = Image.new("RGB", (width, height), color_rgb)
if img_no_bg.mode == 'RGBA':
color_bg_rgba = color_bg.convert("RGBA")
final_img = Image.alpha_composite(color_bg_rgba, img_no_bg)
else:
final_img = img_no_bg
processed_image = final_img
update_display(final_img)
def update_display(img_pil):
if img_pil.mode == 'RGBA':
display_img = Image.new("RGB", img_pil.size, (255, 255, 255))
display_img.paste(img_pil, mask=img_pil.split()[3])
else:
display_img = img_pil
display_img.thumbnail((MAX_DISPLAY_WIDTH, FIXED_DISPLAY_HEIGHT))
photo_new = ImageTk.PhotoImage(display_img)
label_result.config(image=photo_new)
label_result.image = photo_new
def save_img():
global processed_image
if not processed_image:
messagebox.showwarning("提示", "没有可保存的图片")
return
path = filedialog.asksaveasfilename(
defaultextension=".jpg",
filetypes=[("JPEG 图片", "*.jpg"), ("PNG 透明图", "*.png")]
)
if path:
try:
ext = os.path.splitext(path)[1].lower()
if ext in ['.jpg', '.jpeg']:
if processed_image.mode == 'RGBA':
white_bg = Image.new("RGB", processed_image.size, (255, 255, 255))
white_bg.paste(processed_image, mask=processed_image.split()[3])
save_img_obj = white_bg
else:
save_img_obj = processed_image.convert("RGB")
save_img_obj.save(path, quality=95)
else:
processed_image.save(path)
messagebox.showinfo("成功", "图片已保存!")
except Exception as e:
messagebox.showerror("保存失败", f"错误:{str(e)}")
# --- 按钮定义 ---
color_blue = (67, 142, 219)
color_red = (220, 50, 47)
color_white = (255, 255, 255)
btn_style = {"width": 12, "height": 2, "font": ("Arial", 10)}
tk.Button(frame_mid, text="换成蓝底", bg="#add8e6", **btn_style,
command=lambda: change_color(color_blue)).pack(pady=15)
tk.Button(frame_mid, text="换成红底", bg="#ffcccb", **btn_style,
command=lambda: change_color(color_red)).pack(pady=15)
tk.Button(frame_mid, text="换成白底", bg="white", relief="solid", **btn_style,
command=lambda: change_color(color_white)).pack(pady=15)
tk.Frame(frame_mid, height=20).pack() # 占位符
tk.Button(frame_mid, text="保存结果", bg="#dddddd", **btn_style,
command=save_img).pack(pady=30)
# --- 右侧初始化 ---
label_result = tk.Label(frame_right, bg="#e0e0e0")
label_result.pack(expand=True)
change_color(color_white)
if __name__ == "__main__":
root = tk.Tk()
root.withdraw() # 隐藏主窗口,作为模块使用时
start_bg_tool()
root.mainloop()
更多推荐
所有评论(0)