核心功能
将图片中的指定颜色(Color A)精准替换为另一种颜色(Color B),并保留图片的其余部分(包括透明度)。
具体的亮点与细节功能:
可视化文件管理
打开图片:可以通过文件选择框一键导入本地图片(支持 PNG, JPG, BMP, WEBP 等常见格式)。
另存为新图:处理完成后,可以直接将结果保存为新的本地文件(支持保存为保留透明背景的 PNG,或自动转换通道保存为 JPG)。
直观的图片预览区
导入图片后,会自动按比例缩放显示在界面的左侧画布中,不会因为图片太大而撑爆软件窗口。
处理后的结果也会实时显示在这个预览区里。
非常方便的“吸色器”功能(亮点)
直接点击拾色:你不需要去猜某块区域的 RGB 值是多少。只需把鼠标移到左侧的图片预览区,直接点击你想替换掉的颜色区域,程序就会自动读取该点的 RGB 值,并将其设为“被替换颜色 A”。
系统调色板支持
提供了“调色板选择”按钮。点击后会弹出 Windows 系统自带的调色板窗口,让你可以自由地滑动鼠标或者输入色号来挑选“新颜色 B”。
智能容差调节 (Tolerance)
容差滑动条 (0~100):这是为了解决实际图片中颜色不纯的问题(比如 JPG 图片由于压缩产生的杂色,或者图形边缘的抗锯齿渐变色)。
容差为 0:只有 RGB 绝对一模一样的像素才会被替换(适合处理纯色色块的像素风图片)。
容差调大(如 15 或 30):允许把“相近的颜色”一起替换掉,这样替换出来的边缘过渡会更加自然,不会留下明显的毛刺。
一键重置功能
如果替换后的效果不满意(比如容差没调好,导致不该变色的地方也变色了),可以点击“↺ 还原原图”,瞬间恢复到最初的状态重新操作,不需要重新去硬盘里打开图片。
解除了超大图片的限制
代码内部自动解除了 Python 图像库对“一亿像素”以上超大图片的限制防线,因此就算是超大分辨率的设计原图也可以顺利导入,不会报错崩溃。
总结来说: 这是一套可以直接打包发给完全不懂代码的普通用户使用的“傻瓜式”修图小工具,体验很接近 Photoshop 里的“色彩范围”替换功能。
import os
import sys
import tkinter as tk
from tkinter import filedialog, colorchooser, messagebox, ttk
from PIL import Image, ImageTk
解除 Pillow 对超大图片的像素限制 (防止解压炸弹报错)
Image.MAX_IMAGE_PIXELS = None
class ColorReplacerApp:
def init(self, root):
self.root = root
self.root.title(“图片颜色替换工具 (上位机界面)”)
self.root.geometry(“1000x680”) # 修正:去除了空格
self.root.minsize(800, 600)
# 变量初始化
self.image_path = None
self.original_img = None # PIL Image (RGBA)
self.modified_img = None # PIL Image (RGBA)
self.tk_display_img = None # ImageTk.PhotoImage
self.color_a = (255, 255, 255) # 默认原颜色: 白
self.color_b = (0, 0, 0) # 默认新颜色: 黑
self.tolerance = 10 # 容差
# 设置整体样式
self.style = ttk.Style()
self.style.theme_use('clam')
self.create_widgets()
def create_widgets(self):
# 顶栏 - 文件选择
top_frame = ttk.Frame(self.root, padding=10)
top_frame.pack(fill=tk.X)
ttk.Label(top_frame, text="图片路径:").pack(side=tk.LEFT, padx=5)
self.entry_path = ttk.Entry(top_frame)
self.entry_path.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
btn_browse = ttk.Button(top_frame, text="打开图片", command=self.load_image)
btn_browse.pack(side=tk.LEFT, padx=5)
# 中间主体 - 左侧图片预览,右侧控制面板
main_frame = ttk.Frame(self.root, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# --- 左侧:图片画布预览区 ---
preview_frame = ttk.LabelFrame(main_frame, text="图片预览 (点击图片可直接拾取原颜色 A)", padding=10)
preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
self.canvas = tk.Canvas(preview_frame, bg="#e0e0e0", highlightthickness=0)
self.canvas.pack(fill=tk.BOTH, expand=True)
self.canvas.bind("<Button-1>", self.on_canvas_click)
# --- 右侧:控制面板 ---
control_frame = ttk.LabelFrame(main_frame, text="参数设置", padding=15)
# 修正:已移除了 pack 里面的 minsize=280 参数
control_frame.pack(side=tk.RIGHT, fill=tk.Y)
# 颜色 A 设置
frame_color_a = ttk.Frame(control_frame)
frame_color_a.pack(fill=tk.X, pady=10)
ttk.Label(frame_color_a, text="被替换颜色 A:").pack(anchor=tk.W)
self.swatch_a = tk.Label(frame_color_a, text=" RGB(255,255,255) ", bg="#ffffff", fg="#000000", relief=tk.SOLID, bd=1, width=25)
self.swatch_a.pack(fill=tk.X, pady=5)
btn_pick_a = ttk.Button(frame_color_a, text="调色板选择 Color A", command=self.pick_color_a)
btn_pick_a.pack(fill=tk.X)
ttk.Separator(control_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
# 颜色 B 设置
frame_color_b = ttk.Frame(control_frame)
frame_color_b.pack(fill=tk.X, pady=10)
ttk.Label(frame_color_b, text="替换目标颜色 B:").pack(anchor=tk.W)
self.swatch_b = tk.Label(frame_color_b, text=" RGB(0,0,0) ", bg="#000000", fg="#ffffff", relief=tk.SOLID, bd=1, width=25)
self.swatch_b.pack(fill=tk.X, pady=5)
btn_pick_b = ttk.Button(frame_color_b, text="调色板选择 Color B", command=self.pick_color_b)
btn_pick_b.pack(fill=tk.X)
ttk.Separator(control_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
# 容差设置
frame_tol = ttk.Frame(control_frame)
frame_tol.pack(fill=tk.X, pady=10)
self.lbl_tol = ttk.Label(frame_tol, text=f"颜色对比容差: {self.tolerance}")
self.lbl_tol.pack(anchor=tk.W)
self.slider_tol = ttk.Scale(frame_tol, from_=0, to=100, value=self.tolerance, command=self.on_tolerance_change)
self.slider_tol.pack(fill=tk.X, pady=5)
ttk.Separator(control_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=15)
# 操作按钮
btn_process = ttk.Button(control_frame, text="⚡ 执行颜色替换", command=self.process_image)
btn_process.pack(fill=tk.X, ipady=5, pady=5)
btn_save = ttk.Button(control_frame, text="💾 另存为新图片", command=self.save_image)
btn_save.pack(fill=tk.X, ipady=5, pady=5)
btn_reset = ttk.Button(control_frame, text="↺ 还原原图", command=self.reset_image)
btn_reset.pack(fill=tk.X, pady=5)
# 状态栏
self.status_var = tk.StringVar(value="就绪,请选择图片文件。")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, padding=5)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def load_image(self):
file_path = filedialog.askopenfilename(
filetypes=[("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.webp"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
self.image_path = file_path
self.entry_path.delete(0, tk.END)
self.entry_path.insert(0, file_path)
self.original_img = Image.open(file_path).convert("RGBA")
self.modified_img = self.original_img.copy()
self.update_canvas(self.modified_img)
self.status_var.set(f"已加载图片: {os.path.basename(file_path)} ({self.original_img.width}x{self.original_img.height})")
except Exception as e:
messagebox.showerror("错误", f"无法打开图片:\n{e}")
def update_canvas(self, img_to_show):
if img_to_show is None:
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width < 10 or canvas_height < 10:
canvas_width, canvas_height = 600, 500
# 按比例缩放图片适应画布
img_copy = img_to_show.copy()
img_copy.thumbnail((canvas_width, canvas_height), Image.Resampling.LANCZOS)
self.displayed_scale = img_copy.width / img_to_show.width
self.tk_display_img = ImageTk.PhotoImage(img_copy)
self.canvas.delete("all")
# 居中显示
x = (canvas_width - img_copy.width) // 2
y = (canvas_height - img_copy.height) // 2
self.canvas.create_image(x, y, anchor=tk.NW, image=self.tk_display_img)
self.img_offset_x = x
self.img_offset_y = y
self.disp_width = img_copy.width
self.disp_height = img_copy.height
def on_canvas_click(self, event):
"""在图片上点击直接吸色"""
if self.modified_img is None:
return
# 计算点击在图片内部的真实坐标
rel_x = event.x - self.img_offset_x
rel_y = event.y - self.img_offset_y
if 0 <= rel_x < self.disp_width and 0 <= rel_y < self.disp_height:
real_x = int(rel_x / self.displayed_scale)
real_y = int(rel_y / self.displayed_scale)
pixel = self.modified_img.getpixel((real_x, real_y))
self.color_a = pixel[:3]
self.update_swatch_a()
self.status_var.set(f"吸色成功!原坐标 ({real_x}, {real_y}) 颜色 RGB: {self.color_a}")
def pick_color_a(self):
color = colorchooser.askcolor(title="选择需要被替换的颜色 A", color=self.rgb_to_hex(self.color_a))
if color[0]:
self.color_a = tuple(int(c) for c in color[0])
self.update_swatch_a()
def pick_color_b(self):
color = colorchooser.askcolor(title="选择目标新颜色 B", color=self.rgb_to_hex(self.color_b))
if color[0]:
self.color_b = tuple(int(c) for c in color[0])
self.update_swatch_b()
def update_swatch_a(self):
hex_color = self.rgb_to_hex(self.color_a)
fg = "#000000" if sum(self.color_a) > 382 else "#ffffff"
self.swatch_a.config(bg=hex_color, fg=fg, text=f" RGB{self.color_a} ")
def update_swatch_b(self):
hex_color = self.rgb_to_hex(self.color_b)
fg = "#000000" if sum(self.color_b) > 382 else "#ffffff"
self.swatch_b.config(bg=hex_color, fg=fg, text=f" RGB{self.color_b} ")
def on_tolerance_change(self, val):
self.tolerance = int(float(val))
self.lbl_tol.config(text=f"颜色对比容差: {self.tolerance}")
def process_image(self):
if self.original_img is None:
messagebox.showwarning("提示", "请先加载图片!")
return
self.status_var.set("正在处理像素颜色替换...")
self.root.update_idletasks()
data = self.original_img.getdata()
new_data = []
t_r, t_g, t_b = self.color_a
n_r, n_g, n_b = self.color_b
tol = self.tolerance
for item in data:
r, g, b, a = item
if abs(r - t_r) <= tol and abs(g - t_g) <= tol and abs(b - t_b) <= tol:
new_data.append((n_r, n_g, n_b, a))
else:
new_data.append(item)
self.modified_img = Image.new("RGBA", self.original_img.size)
self.modified_img.putdata(new_data)
self.update_canvas(self.modified_img)
self.status_var.set("颜色替换完成!可以点击保存图片。")
def reset_image(self):
if self.original_img:
self.modified_img = self.original_img.copy()
self.update_canvas(self.modified_img)
self.status_var.set("已重置为原图。")
def save_image(self):
if self.modified_img is None:
messagebox.showwarning("提示", "没有可保存的图片!")
return
save_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("所有文件", "*.*")]
)
if save_path:
try:
if save_path.lower().endswith(('.jpg', '.jpeg')):
self.modified_img.convert("RGB").save(save_path)
else:
self.modified_img.save(save_path)
messagebox.showinfo("成功", f"图片保存成功:\n{save_path}")
self.status_var.set(f"已保存至: {save_path}")
except Exception as e:
messagebox.showerror("错误", f"保存图片失败:\n{e}")
@staticmethod
def rgb_to_hex(rgb):
return "#%02x%02x%02x" % rgb
if name == “main“:
root = tk.Tk()
app = ColorReplacerApp(root)
root.mainloop()