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

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

zonemu2个月前 (08-18)技术文章29

功能:

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

实现代码:

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()

工具效果:

相关文章

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

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

基于Docker构建安装Git/GitLab,以及制作springboot工程镜像

今天给大家分享的是《领先的开源自动化服务器Jenkins的应用实战》之基于Docker安装构建Git/GitLab版本控制与代码云存储的场所;使用Git管理项目,springboot工程制作镜像知识体...

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

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

HTML5设计与制作哪家强?全省50多所高职院校齐聚中山比拼

3月22日下午,2018-2019年广东省职业院校学生专业技能大赛“HTML5交互融媒体内容设计与制作”赛项在中山火炬职业技术学院开幕。全省51所高职院校的52支参赛队伍参加此次大赛。参赛师生将于3月...

Web开发的十佳HTML5响应式框架(h5响应式模板)

HTML5框架是一类有助于快速轻松创建响应式网站的程序包。这些HTML5框架有着能减轻编程任务和重复代码负担的神奇功能。关于HTML5的框架种类繁多,并且很瘦欢迎,因为它能允许开发人员花费更少的时间和...

js中数组filter方法的使用和实现(js实现数组的indexof方法)

定义filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。语法var newArray = arr.filter(callback(element[, index[, se...