颜色替换

核心功能
将图片中的指定颜色(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()