用python + pillow实现GUI界面图片GUI处理工具

用python + pillow实现GUI界面图片GUI处理工具

该工具采用Tkinter构建GUI界面,功能:

调整图片大小

转换图片格式保存

图片裁剪、旋转、翻转

亮度 / 对比度 / 饱和度调整

滤镜效果(模糊、锐化、黑白等)

运行截图:

安装依赖(第三方库):

pip install pillow

Python第三方模块(库、包)安装、卸载与查看及常见问题解决,可参见 https://blog.csdn.net/cnds123/article/details/104393385

Pillow 上手简单,适合绝大多数日常图像处理场景(比如裁剪、调色、加滤镜),它是经典的 PIL(Python Imaging Library)的活跃分支,也是 PIL 停止维护后的官方替代方案。需要注意的是:安装时(新手要特别注意)需要执行 pip install pillow 命令,但在代码中导入库时,必须使用 import PIL(而非 import pillow)------ 这是因为 Pillow 完全兼容 PIL 的 API 命名体系,保留了"PIL"这个导入标识,仅在包安装层面使用"pillow"名称。

pillow库(PIL库)的使用https://blog.csdn.net/cnds123/article/details/126141838

官网 https://pillow.readthedocs.io/en/stable/

中文参靠 https://pillow-docs-cn.readthedocs.io/zh-cn/latest/https://osgeo.cn/pillow/reference/index.html

源码如下:

python 复制代码
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from PIL import Image, ImageTk, ImageEnhance, ImageFilter

# ====================== 调整大小弹窗 ======================
class ResizePopup:
    def __init__(self, parent, original_img, update_callback):
        self.top = tk.Toplevel(parent)
        self.top.title("调整大小")
        self.top.geometry("560x310")
        self.top.resizable(False, False)
        self.top.transient(parent)
        self.top.grab_set()

        self.parent = parent
        self.original_img = original_img
        self.update_callback = update_callback
        self.org_w, self.org_h = original_img.size

        self.width_var = tk.StringVar(value=str(self.org_w))
        self.height_var = tk.StringVar(value=str(self.org_h))
        self.quick_scale_var = tk.StringVar(value="100%")
        self.maintain_ratio = tk.BooleanVar(value=True)
        self.resample = tk.BooleanVar(value=True)
        self.quick_scales = ["25%", "50%", "75%", "100%", "125%", "150%", "200%"]

        self._sync_locked = False
        self.create_widgets()
        self.bind_events()

    def create_widgets(self):
        main_frame = tk.Frame(self.top, padx=12, pady=12)
        main_frame.pack(fill=tk.BOTH, expand=True)

        left_frame = tk.LabelFrame(main_frame, text="尺寸设置", font=("微软雅黑", 10))
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)

        tk.Label(left_frame, text="宽度", font=("微软雅黑", 10)).grid(row=0, column=0, padx=10, pady=10, sticky="w")
        tk.Entry(left_frame, textvariable=self.width_var, font=("微软雅黑", 10), width=10).grid(row=0, column=1, padx=5)
        tk.Label(left_frame, text="像素").grid(row=0, column=2, sticky="w")

        tk.Label(left_frame, text="高度", font=("微软雅黑", 10)).grid(row=1, column=0, padx=10, pady=8, sticky="w")
        tk.Entry(left_frame, textvariable=self.height_var, font=("微软雅黑", 10), width=10).grid(row=1, column=1, padx=5)
        tk.Label(left_frame, text="像素").grid(row=1, column=2, sticky="w")

        tk.Label(left_frame, text="快速缩放", font=("微软雅黑", 10)).grid(row=2, column=0, padx=10, pady=10, sticky="w")
        self.quick_combo = ttk.Combobox(left_frame, textvariable=self.quick_scale_var, values=self.quick_scales, font=("微软雅黑", 10), width=7, state="readonly")
        self.quick_combo.grid(row=2, column=1, padx=5, sticky="w")

        tk.Checkbutton(left_frame, text="保持宽高比", variable=self.maintain_ratio, font=("微软雅黑", 10)).grid(row=3, column=0, columnspan=3, padx=10, pady=5, sticky="w")
        tk.Checkbutton(left_frame, text="高质量重采样", variable=self.resample, font=("微软雅黑", 10)).grid(row=4, column=0, columnspan=3, padx=10, pady=5, sticky="w")

        right_frame = tk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=10)
        tk.Button(right_frame, text="还原", width=8, height=2, command=self.restore_size).pack(pady=6)
        tk.Button(right_frame, text="确定", width=8, height=2, command=self.on_ok).pack(pady=6)
        tk.Button(right_frame, text="取消", width=8, height=2, command=self.top.destroy).pack(pady=6)

    def bind_events(self):
        self.width_var.trace_add("write", self.sync_size)
        self.height_var.trace_add("write", self.sync_size)
        self.quick_combo.bind("<<ComboboxSelected>>", self.on_quick_scale)

    def on_quick_scale(self, e=None):
        try:
            s = float(self.quick_scale_var.get().replace("%", "")) / 100
            w, h = int(self.org_w * s), int(self.org_h * s)
            self.width_var.set(str(w))
            self.height_var.set(str(h))
        except:
            pass

    def sync_size(self, *args):
        if not self.maintain_ratio.get() or self._sync_locked:
            return
        try:
            self._sync_locked = True
            w = int(self.width_var.get())
            h = int(w * self.org_h / self.org_w)
            self.height_var.set(str(h))
        except:
            pass
        finally:
            self._sync_locked = False

    def restore_size(self):
        self.width_var.set(str(self.org_w))
        self.height_var.set(str(self.org_h))
        self.quick_scale_var.set("100%")

    def on_ok(self):
        try:
            w, h = int(self.width_var.get()), int(self.height_var.get())
            method = Image.Resampling.LANCZOS if self.resample.get() else Image.Resampling.NEAREST
            self.update_callback(self.original_img.resize((w, h), method))
        except:
            messagebox.showerror("错误", "尺寸无效")
        self.top.destroy()

# ====================== 主程序(美观紧凑版)======================
class ImageTool:
    def __init__(self, root):
        self.root = root
        self.root.title("全能图片处理工具")
        self.root.geometry("1180x800")
        self.root.minsize(900, 650)

        self.original_img = None
        self.base_img = None
        self.current_img = None
        self.tk_img = None

        self.is_cropping = False
        self.crop_rect = None

        self.build_ui()

    def build_ui(self):
        # ========== 主容器 ==========
        main_container = tk.Frame(self.root, padx=10, pady=6)
        main_container.pack(fill=tk.BOTH, expand=True)

        # ========== 功能栏一行 ==========
        func_bar = tk.Frame(main_container)
        func_bar.pack(fill=tk.X, pady=4)

        # 左:文件操作
        file_group = tk.LabelFrame(func_bar, text="文件操作", padx=6, pady=4)
        file_group.pack(side=tk.LEFT, padx=4)
        tk.Button(file_group, text="打开", width=8, command=self.open_img).pack(side=tk.LEFT, padx=3)
        tk.Button(file_group, text="保存", width=8, command=self.save_img).pack(side=tk.LEFT, padx=3)
        tk.Button(file_group, text="重置", width=8, command=self.reset_original).pack(side=tk.LEFT, padx=3)

        # 中:几何变换
        transform_group = tk.LabelFrame(func_bar, text="几何变换", padx=6, pady=4)
        transform_group.pack(side=tk.LEFT, padx=4)
        tk.Button(transform_group, text="调整大小", width=8, command=self.open_resize).pack(side=tk.LEFT, padx=3)
        tk.Button(transform_group, text="裁剪", width=6, command=self.start_crop).pack(side=tk.LEFT, padx=3)
        tk.Button(transform_group, text="左转", width=6, command=lambda: self.rotate(90)).pack(side=tk.LEFT, padx=3)
        tk.Button(transform_group, text="右转", width=6, command=lambda: self.rotate(-90)).pack(side=tk.LEFT, padx=3)
        tk.Button(transform_group, text="左右翻转", width=8, command=self.flip_h).pack(side=tk.LEFT, padx=3)
        tk.Button(transform_group, text="上下翻转", width=8, command=self.flip_v).pack(side=tk.LEFT, padx=3)

        # 右:滤镜
        filter_group = tk.LabelFrame(func_bar, text="滤镜", padx=6, pady=4)
        filter_group.pack(side=tk.LEFT, padx=4)
        tk.Button(filter_group, text="模糊", width=6, command=self.filter_blur).pack(side=tk.LEFT, padx=3)
        tk.Button(filter_group, text="锐化", width=6, command=self.filter_sharpen).pack(side=tk.LEFT, padx=3)
        tk.Button(filter_group, text="黑白", width=6, command=self.filter_gray).pack(side=tk.LEFT, padx=3)
        tk.Button(filter_group, text="复古", width=6, command=self.filter_sepia).pack(side=tk.LEFT, padx=3)
        tk.Button(filter_group, text="浮雕", width=6, command=self.filter_emboss).pack(side=tk.LEFT, padx=3)

        # ========== 图像增强 ==========
        enhance_bar = tk.Frame(main_container)
        enhance_bar.pack(fill=tk.X, pady=6)
  
        enhance_group = tk.LabelFrame(enhance_bar, text="图像增强(实时调节)", padx=10, pady=6)
        enhance_group.pack(fill=tk.X, expand=True)

        tk.Label(enhance_group, text="亮度").grid(row=0, column=0, padx=5)
        self.bright = tk.Scale(enhance_group, from_=0, to=3, resolution=0.1, length=200, orient=tk.HORIZONTAL, command=self.update_enhance)
        self.bright.grid(row=0, column=1, padx=5)
        self.bright.set(1.0)

        tk.Label(enhance_group, text="对比度").grid(row=0, column=2, padx=5)
        self.contrast = tk.Scale(enhance_group, from_=0, to=3, resolution=0.1, length=200, orient=tk.HORIZONTAL, command=self.update_enhance)
        self.contrast.grid(row=0, column=3, padx=5)
        self.contrast.set(1.0)

        tk.Label(enhance_group, text="饱和度").grid(row=0, column=4, padx=5)
        self.satur = tk.Scale(enhance_group, from_=0, to=3, resolution=0.1, length=200, orient=tk.HORIZONTAL, command=self.update_enhance)
        self.satur.grid(row=0, column=5, padx=5)
        self.satur.set(1.0)

        # ========== 图片显示区域 ==========
        canvas_container = tk.LabelFrame(main_container, text="预览区域", padx=6, pady=4)
        canvas_container.pack(fill=tk.BOTH, expand=True, pady=4)

        self.canvas_frame = tk.Frame(canvas_container)
        self.canvas_frame.pack(fill=tk.BOTH, expand=True)

        self.sx = tk.Scrollbar(self.canvas_frame, orient=tk.HORIZONTAL)
        self.sy = tk.Scrollbar(self.canvas_frame, orient=tk.VERTICAL)
        self.canvas = tk.Canvas(self.canvas_frame, bg="#f5f5f5", xscrollcommand=self.sx.set, yscrollcommand=self.sy.set)
        self.sx.config(command=self.canvas.xview)
        self.sy.config(command=self.canvas.yview)

        self.sx.pack(side=tk.BOTTOM, fill=tk.X)
        self.sy.pack(side=tk.RIGHT, fill=tk.Y)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.canvas.bind("<ButtonPress-1>", self.on_crop_press)
        self.canvas.bind("<B1-Motion>", self.on_crop_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_crop_release)

    # ========== 基础功能 ==========
    def open_img(self):
        path = filedialog.askopenfilename(filetypes=[
            ("所有图片", "*.jpg *.jpeg *.png *.bmp *.webp *.tiff"),
            ("JPG", "*.jpg;*.jpeg"), ("PNG", "*.png"), ("BMP", "*.bmp"), ("WebP", "*.webp")
        ])
        if not path:
            return
        try:
            self.original_img = Image.open(path).convert("RGBA")
            self.base_img = self.original_img.copy()
            self.update_enhance()
        except Exception as e:
            messagebox.showerror("错误", f"打开失败:{str(e)}")

    def show_img(self):
        if not self.current_img:
            return
        w, h = self.current_img.size
        self.tk_img = ImageTk.PhotoImage(self.current_img)
        self.canvas.delete("all")
        self.canvas.create_image(0, 0, image=self.tk_img, anchor=tk.NW)
        self.canvas.config(scrollregion=(0, 0, w, h))

    def reset_original(self):
        if self.original_img:
            self.base_img = self.original_img.copy()
            self.bright.set(1.0)
            self.contrast.set(1.0)
            self.satur.set(1.0)
            self.update_enhance()

    # ========== 裁剪 ==========
    def start_crop(self):
        if not self.base_img:
            messagebox.showwarning("提示", "请先打开图片")
            return
        self.is_cropping = True
        messagebox.showinfo("提示", "在图片上拖拽框选裁剪区域")

    def on_crop_press(self, e):
        if not self.is_cropping:
            return
        self.crop_start_x = self.canvas.canvasx(e.x)
        self.crop_start_y = self.canvas.canvasy(e.y)
        if self.crop_rect:
            self.canvas.delete(self.crop_rect)
        self.crop_rect = self.canvas.create_rectangle(0, 0, 0, 0, outline="red", dash=(4, 2), width=2)

    def on_crop_drag(self, e):
        if not self.is_cropping or not self.crop_rect:
            return
        cx = self.canvas.canvasx(e.x)
        cy = self.canvas.canvasy(e.y)
        self.canvas.coords(self.crop_rect, self.crop_start_x, self.crop_start_y, cx, cy)

    def on_crop_release(self, e):
        if not self.is_cropping or not self.crop_rect:
            return
        try:
            x1, y1, x2, y2 = map(int, self.canvas.coords(self.crop_rect))
            if x2 <= x1 or y2 <= y1:
                self.canvas.delete(self.crop_rect)
                self.is_cropping = False
                return
            self.base_img = self.base_img.crop((x1, y1, x2, y2))
            self.update_enhance()
            self.is_cropping = False
        except:
            self.canvas.delete(self.crop_rect)
            self.is_cropping = False

    # ========== 调整大小 ==========
    def open_resize(self):
        if not self.base_img:
            messagebox.showwarning("提示", "请先打开图片")
            return
        ResizePopup(self.root, self.base_img, self.update_base_img)

    def update_base_img(self, img):
        self.base_img = img
        self.update_enhance()

    # ========== 旋转 / 翻转 ==========
    def rotate(self, angle):
        if self.base_img:
            self.base_img = self.base_img.rotate(angle, expand=True)
            self.update_enhance()

    def flip_h(self):
        if self.base_img:
            self.base_img = self.base_img.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
            self.update_enhance()

    def flip_v(self):
        if self.base_img:
            self.base_img = self.base_img.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
            self.update_enhance()

    # ========== 滤镜 ==========
    def filter_blur(self):
        if self.base_img:
            self.base_img = self.base_img.filter(ImageFilter.GaussianBlur(2))
            self.update_enhance()

    def filter_sharpen(self):
        if self.base_img:
            self.base_img = self.base_img.filter(ImageFilter.SHARPEN)
            self.update_enhance()

    def filter_gray(self):
        if self.base_img:
            self.base_img = self.base_img.convert("L").convert("RGBA")
            self.update_enhance()

    def filter_sepia(self):
        if self.base_img:
            img = self.base_img.convert("RGB")
            pixels = img.load()
            w, h = img.size
            for i in range(w):
                for j in range(h):
                    r, g, b = img.getpixel((i, j))
                    tr = int(0.393 * r + 0.769 * g + 0.189 * b)
                    tg = int(0.349 * r + 0.686 * g + 0.168 * b)
                    tb = int(0.272 * r + 0.534 * g + 0.131 * b)
                    pixels[i, j] = (min(tr, 255), min(tg, 255), min(tb, 255))
            self.base_img = img.convert("RGBA")
            self.update_enhance()

    def filter_emboss(self):
        if self.base_img:
            self.base_img = self.base_img.filter(ImageFilter.EMBOSS)
            self.update_enhance()

    # ========== 增强 ==========
    def update_enhance(self, *args):
        if not self.base_img:
            return
        img = self.base_img.copy()
        img = ImageEnhance.Brightness(img).enhance(self.bright.get())
        img = ImageEnhance.Contrast(img).enhance(self.contrast.get())
        img = ImageEnhance.Color(img).enhance(self.satur.get())
        self.current_img = img
        self.show_img()

    # ========== 保存 ==========
    def save_img(self):
        if not self.current_img:
            messagebox.showwarning("提示", "无图片可保存")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[
            ("PNG", "*.png"), ("JPG", "*.jpg"), ("WebP", "*.webp"), ("BMP", "*.bmp")
        ])
        if not path:
            return
        try:
            img = self.current_img
            if path.lower().endswith(("jpg", "jpeg")):
                bg = Image.new("RGB", img.size, (255, 255, 255))
                bg.paste(img, mask=img.split()[-1])
                bg.save(path)
            else:
                img.save(path)
            messagebox.showinfo("成功", "图片已保存")
        except Exception as e:
            messagebox.showerror("错误", f"保存失败:{str(e)}")

if __name__ == "__main__":
    root = tk.Tk()
    ImageTool(root)
    root.mainloop()

附录

用python + PIL 实现图片格式转换工具 https://blog.csdn.net/cnds123/article/details/146922310

相关推荐
weixin_425023002 小时前
PG JSONB 对应 Java 字段 + MyBatis-Plus 完整实战
java·开发语言·mybatis
FreakStudio2 小时前
ESP32 实现在线动态安装库和自动依赖安装-使用uPyPI包管理平台
python·单片机·嵌入式·面向对象·电子diy·sourcetrail
别抢我的锅包肉2 小时前
【FastAPI】 响应类型详解:从默认 JSON 到自定义响应
python·fastapi
leaves falling2 小时前
C++ string 类:从入门到模拟实现
开发语言·c++
智算菩萨2 小时前
【Tkinter】15 样式与主题深度解析:ttk 主题系统、Style 对象与跨平台样式管理实战
开发语言·python·ui·ai编程·tkinter
子非鱼@Itfuture2 小时前
`<T> T execute(...)` 泛型方法 VS `TaskExecutor<T>` 泛型接口对比分析
java·开发语言
weixin_419349793 小时前
Python 项目中生成 requirements.txt 文件
开发语言·python
林恒smileZAZ3 小时前
前端大屏适配方案:rem、vw/vh、scale 到底选哪个?
开发语言·前端·css·css3
第一程序员3 小时前
Python与区块链:非科班转码者的指南
python·github