当前位置:首页 > 技术文章 > 正文内容

【Python】图片内容清除工具,附源码

zonemu1周前 (08-18)技术文章12

功能:

  • 快速清除一张图片不需要的内容,鼠标选中松开即清除
  • 智能填充背景色,以选择边框占比多的颜色进行填充
  • 对于同级目录多个要清除的图片,可直接“下一张”,节省操作时间

实现代码:

import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk, ImageDraw
import os
import glob
from collections import Counter

class AdvancedImageEraser:
    def __init__(self, root):
        self.root = root
        self.root.title("图片编辑工具")
        self.root.geometry("1100x750")
        self.root.configure(bg="#2c3e50")
        self.root.minsize(800, 600)  # 最小窗口尺寸
        
        # 初始化变量
        self.image_path = None
        self.image_dir = None
        self.image_files = []
        self.current_index = -1
        self.original_image = None
        self.current_image = None
        self.display_image = None
        self.display_photo = None
        self.start_x = None
        self.start_y = None
        self.rect = None
        self.bg_color = (255, 255, 255)  # 默认白色背景
        self.scale_factor = 1.0  # 图片缩放因子
        
        # 创建UI
        self.create_widgets()
        
        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪 | 请打开一张图片")
        status_bar = tk.Label(root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, 
                             anchor=tk.W, bg="#34495e", fg="white")
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 绑定窗口大小变化事件
        self.root.bind("<Configure>", self.on_window_resize)
    
    def create_widgets(self):
        # 顶部工具栏
        toolbar = tk.Frame(self.root, bg="#34495e", height=40)
        toolbar.pack(side=tk.TOP, fill=tk.X)
        
        # 按钮样式
        btn_style = {"bg": "#3498db", "fg": "white", "bd": 0, "padx": 10, "pady": 5}
        
        tk.Button(toolbar, text="打开图片", command=self.open_image, **btn_style).pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="保存图片", command=self.save_image, **btn_style).pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="重置", command=self.reset_image, **btn_style).pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="上一张", command=self.prev_image, **btn_style).pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="下一张", command=self.next_image, **btn_style).pack(side=tk.LEFT, padx=5)
        
        # 图片显示区域 - 添加滚动条
        image_frame = tk.Frame(self.root, bg="#2c3e50")
        image_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 创建画布和滚动条
        self.canvas = tk.Canvas(image_frame, bg="#34495e", bd=0, highlightthickness=0)
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        # 绑定鼠标事件
        self.canvas.bind("<ButtonPress-1>", self.on_press)
        self.canvas.bind("<B1-Motion>", self.on_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_release)
        
        # 操作说明
        help_text = """
        使用说明:
        1. 点击"打开图片"按钮选择要编辑的图片
        2. 在图片上按住鼠标左键并拖动选择要抹除的区域
        3. 释放鼠标左键,选中的区域会被边缘主要颜色替换
        4. 点击"保存图片"将修改保存到原文件
        5. 使用"上一张"/"下一张"浏览同级目录图片
        6. 图片会自动缩放以适应窗口大小
        """
        tk.Label(image_frame, text=help_text, bg="#2c3e50", fg="#bdc3c7", 
                justify=tk.LEFT).pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)
    
    def get_dominant_border_color(self, x1, y1, x2, y2):
        """获取选中区域边缘的主要颜色"""
        if not self.original_image:
            return (255, 255, 255)  # 默认白色
        
        # 将缩放后的坐标转换为原始坐标
        orig_x1 = int(x1 / self.scale_factor)
        orig_y1 = int(y1 / self.scale_factor)
        orig_x2 = int(x2 / self.scale_factor)
        orig_y2 = int(y2 / self.scale_factor)
        
        # 确保坐标在图片范围内
        width, height = self.original_image.size
        orig_x1 = max(0, min(orig_x1, width))
        orig_y1 = max(0, min(orig_y1, height))
        orig_x2 = max(0, min(orig_x2, width))
        orig_y2 = max(0, min(orig_y2, height))
        
        # 收集边框区域的所有像素颜色
        color_samples = []
        border_size = 1  # 使用固定1像素边框
        
        # 上边框 (从左到右)
        for x in range(orig_x1, orig_x2, max(1, (orig_x2 - orig_x1) // 100)):
            for y_offset in range(border_size):
                y = orig_y1 + y_offset
                if y < height:
                    try:
                        color = self.original_image.getpixel((x, y))
                        if len(color) == 4:  # RGBA格式
                            color = color[:3]  # 只取RGB
                        color_samples.append(color)
                    except:
                        pass
        
        # 下边框 (从左到右)
        for x in range(orig_x1, orig_x2, max(1, (orig_x2 - orig_x1) // 100)):
            for y_offset in range(border_size):
                y = orig_y2 - y_offset
                if y >= 0:
                    try:
                        color = self.original_image.getpixel((x, y))
                        if len(color) == 4:  # RGBA格式
                            color = color[:3]  # 只取RGB
                        color_samples.append(color)
                    except:
                        pass
        
        # 左边框 (从上到下)
        for y in range(orig_y1, orig_y2, max(1, (orig_y2 - orig_y1) // 100)):
            for x_offset in range(border_size):
                x = orig_x1 + x_offset
                if x < width:
                    try:
                        color = self.original_image.getpixel((x, y))
                        if len(color) == 4:  # RGBA格式
                            color = color[:3]  # 只取RGB
                        color_samples.append(color)
                    except:
                        pass
        
        # 右边框 (从上到下)
        for y in range(orig_y1, orig_y2, max(1, (orig_y2 - orig_y1) // 100)):
            for x_offset in range(border_size):
                x = orig_x2 - x_offset
                if x >= 0:
                    try:
                        color = self.original_image.getpixel((x, y))
                        if len(color) == 4:  # RGBA格式
                            color = color[:3]  # 只取RGB
                        color_samples.append(color)
                    except:
                        pass
        
        if not color_samples:
            return (255, 255, 255)  # 默认白色
        
        # 找到最常见的颜色
        color_counter = Counter(color_samples)
        dominant_color = color_counter.most_common(1)[0][0]
        
        return dominant_color
    
    def resize_image(self, event=None):
        """调整图片大小以适应窗口"""
        if not self.original_image:
            return
            
        # 获取可用空间 (减去状态栏、工具栏和边距)
        toolbar_height = 40
        statusbar_height = 20
        margin = 40
        available_width = self.root.winfo_width() - 40
        available_height = self.root.winfo_height() - toolbar_height - statusbar_height - margin
        
        # 计算缩放比例
        img_width, img_height = self.original_image.size
        width_ratio = available_width / img_width
        height_ratio = available_height / img_height
        self.scale_factor = min(width_ratio, height_ratio, 1.0)  # 最大缩放100%
        
        # 计算新尺寸
        new_width = int(img_width * self.scale_factor)
        new_height = int(img_height * self.scale_factor)
        
        # 缩放图片
        if self.scale_factor < 1.0:
            resized_img = self.current_image.resize(
                (new_width, new_height), 
                Image.Resampling.LANCZOS
            )
        else:
            resized_img = self.current_image.copy()
        
        # 更新显示
        self.display_photo = ImageTk.PhotoImage(resized_img)
        self.canvas.config(width=new_width, height=new_height)
        self.canvas.create_image(0, 0, anchor=tk.NW, image=self.display_photo)
        
        # 更新状态
        if self.scale_factor < 1.0:
            self.status_var.set(f"图片已缩放: {int(self.scale_factor*100)}% | 原始尺寸: {img_width}x{img_height}")
    
    def open_image(self, path=None):
        """打开图片文件"""
        if not path:
            path = filedialog.askopenfilename(
                filetypes=[("图片文件", "*.jpg *.jpeg *.png *.bmp *.gif")]
            )
        
        if not path:
            return
            
        try:
            self.image_path = path
            self.image_dir = os.path.dirname(path)
            
            # 获取目录中所有图片文件
            all_jpg = glob.glob(os.path.join(self.image_dir, "*.jpg"))
            all_jpeg = glob.glob(os.path.join(self.image_dir, "*.jpeg"))
            all_png = glob.glob(os.path.join(self.image_dir, "*.png"))
            all_bmp = glob.glob(os.path.join(self.image_dir, "*.bmp"))
            all_gif = glob.glob(os.path.join(self.image_dir, "*.gif"))
            
            # 合并所有图片文件并排序
            self.image_files = sorted(all_jpg + all_jpeg + all_png + all_bmp + all_gif)
            
            # 获取当前图片索引
            if self.image_files:
                try:
                    self.current_index = self.image_files.index(path)
                except ValueError:
                    # 如果文件不在列表中,尝试添加它
                    if os.path.exists(path):
                        self.image_files.append(path)
                        self.image_files = sorted(self.image_files)
                        self.current_index = self.image_files.index(path)
                    else:
                        self.current_index = -1
            else:
                self.current_index = -1
            
            # 打开原始图片
            self.original_image = Image.open(path).convert("RGB")
            self.current_image = self.original_image.copy()
            
            # 调整图片大小以适应窗口
            self.resize_image()
            
            # 更新状态
            status = f"已打开: {os.path.basename(path)} | 尺寸: {self.original_image.width}x{self.original_image.height}"
            if self.current_index >= 0 and len(self.image_files) > 1:
                status += f" | 图片 {self.current_index+1}/{len(self.image_files)}"
            self.status_var.set(status)
            
            # 设置窗口标题
            self.root.title(f"图片编辑工具 - {os.path.basename(path)}")
            
        except Exception as e:
            messagebox.showerror("错误", f"无法打开图片: {str(e)}")
            self.status_var.set(f"错误: 无法打开图片 - {str(e)}")
    
    def reset_image(self):
        """重置图片到原始状态"""
        if self.original_image:
            self.current_image = self.original_image.copy()
            self.resize_image()
            self.status_var.set("图片已重置")
    
    def save_image(self):
        """保存图片到原文件"""
        if not self.image_path:
            messagebox.showwarning("警告", "请先打开一张图片")
            return
            
        if not self.current_image:
            messagebox.showwarning("警告", "没有可保存的图片")
            return
            
        try:
            # 保存到原文件
            self.current_image.save(self.image_path)
            
            # 更新状态
            self.status_var.set(f"图片已保存到: {os.path.basename(self.image_path)}")
            
            # 更新原始图片
            self.original_image = self.current_image.copy()
            
            messagebox.showinfo("保存成功", "图片已成功保存到原文件")
        except Exception as e:
            messagebox.showerror("错误", f"保存失败: {str(e)}")
            self.status_var.set(f"保存失败: {str(e)}")
    
    def prev_image(self):
        """打开同级目录的上一张图片"""
        if not self.image_files or self.current_index < 0:
            messagebox.showinfo("提示", "请先打开一张图片")
            return
            
        # 计算上一张图片索引
        prev_index = (self.current_index - 1) % len(self.image_files)
        
        # 打开上一张图片
        self.open_image(self.image_files[prev_index])
    
    def next_image(self):
        """打开同级目录的下一张图片"""
        if not self.image_files or self.current_index < 0:
            messagebox.showinfo("提示", "请先打开一张图片")
            return
            
        # 计算下一张图片索引
        next_index = (self.current_index + 1) % len(self.image_files)
        
        # 打开下一张图片
        self.open_image(self.image_files[next_index])
    
    def on_window_resize(self, event):
        """窗口大小变化时重新调整图片大小"""
        if self.original_image:
            self.resize_image()
    
    def on_press(self, event):
        """鼠标按下事件"""
        if not self.display_photo:
            return
            
        self.start_x = self.canvas.canvasx(event.x)
        self.start_y = self.canvas.canvasy(event.y)
        
        # 创建矩形选择框
        self.rect = self.canvas.create_rectangle(
            self.start_x, self.start_y, self.start_x, self.start_y,
            outline="#e74c3c", width=2, dash=(4, 4)
        )
    
    def on_drag(self, event):
        """鼠标拖动事件"""
        if self.rect is None:
            return
            
        cur_x = self.canvas.canvasx(event.x)
        cur_y = self.canvas.canvasy(event.y)
        
        # 更新矩形选择框
        self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y)
    
    def on_release(self, event):
        """鼠标释放事件"""
        if self.rect is None or self.start_x is None or self.start_y is None:
            return
            
        end_x = self.canvas.canvasx(event.x)
        end_y = self.canvas.canvasy(event.y)
        
        # 确保坐标在图像范围内
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        x1 = max(0, min(self.start_x, end_x))
        y1 = max(0, min(self.start_y, end_y))
        x2 = min(canvas_width, max(self.start_x, end_x))
        y2 = min(canvas_height, max(self.start_y, end_y))
        
        # 确保有选择区域
        if abs(x2 - x1) < 5 or abs(y2 - y1) < 5:
            self.canvas.delete(self.rect)
            self.rect = None
            return
        
        # 获取边缘主要颜色
        self.bg_color = self.get_dominant_border_color(x1, y1, x2, y2)
        
        # 计算原始坐标
        orig_x1 = int(x1 / self.scale_factor)
        orig_y1 = int(y1 / self.scale_factor)
        orig_x2 = int(x2 / self.scale_factor)
        orig_y2 = int(y2 / self.scale_factor)
        
        # 在原始图像上绘制矩形
        draw = ImageDraw.Draw(self.current_image)
        draw.rectangle([orig_x1, orig_y1, orig_x2, orig_y2], fill=self.bg_color)
        
        # 更新显示
        self.resize_image()
        
        # 删除选择框
        self.canvas.delete(self.rect)
        self.rect = None
        
        # 更新状态
        orig_width = orig_x2 - orig_x1
        orig_height = orig_y2 - orig_y1
        status = f"已抹除区域: ({orig_x1}, {orig_y1}) - ({orig_x2}, {orig_y2}) | 大小: {orig_width}x{orig_height} 像素"
        status += f" | 背景色: RGB{self.bg_color}"
        self.status_var.set(status)

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

工具效果:

相关文章

Vue3开发极简入门(14):组件间通信之props、ref&amp;defineExpose

组件间的关系可以分为:父子关系。以前文的代码为例,最典型的就是App.vue与Car.vue这种,APP是父,Car是子。祖孙关系。如果Car再引入一个子组件,这个子组件与App就是祖孙关系。其他。比...

程序员项目经理如何调动组员积极性

#这个方法应该很适合程序员都说程序员是比较傲娇,有点小自负(有的是相当,那不叫自负,那是实力的体现好吗),略微呆萌,自尊心偏小强的一类族群。是吗?中招了吗?作为管理好几个组员,要完成一个大项目的项目经...

前端学习又一大里程碑:html5+js写出歌词同步手机播放器

需要完整代码和视频请评论后加前端群470593776领取javascript进阶课题:HTML5迷你音乐播放器学习疲惫了,代码敲累了,听听自己做的的音乐播放器,放松与满足知识点:for循环语句,DOM...

最快认知什么才是HTML5广告!(h5广告设计是什么)

H5广告似乎是自UI风靡之后,又一个热度极高的词儿。他是什么?一个字母加一个数字是个什么意思? 为什么如此受欢迎?金色号角会议室,创作事业部赵阳同学就HTML5广告做了详尽生动的分享,带大家一起用手机...

Hutool JSONUtil巧妙过滤null值:JSON转Map数据清洗的终极方案

Hutool JSONUtil巧妙过滤null值:JSON转Map数据清洗的终极解决方案声明本文中的所有案例代码、配置仅供参考,如需使用请严格做好相关测试及评估,对于因参照本文内容进行操作而导致的任何...

12种JavaScript中最常用的数组操作整理汇总

数组是最常见的数据结构之一,我们需要绝对自信地使用它。在这里,我将列出 JavaScript 中最重要的几个数组常用操作片段,包括数组长度、替换元素、去重以及许多其他内容。1、数组长度大多数人都知道可...