282 lines
11 KiB
Python
282 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
图像分割工具
|
||
将图片切割成指定大小的小块
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from tkinter import Tk, Label, Button, Entry, filedialog, messagebox, Frame, Scrollbar, Listbox, VERTICAL, HORIZONTAL, BOTH, END, StringVar, Radiobutton
|
||
from PIL import Image
|
||
|
||
class ImageSlicerApp:
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("图像分割工具")
|
||
self.root.geometry("600x500")
|
||
self.root.resizable(True, True)
|
||
|
||
# 初始化变量
|
||
self.source_image_path = ""
|
||
self.slice_size = 512
|
||
self.output_dir = os.path.join(os.path.expanduser("~"), "Desktop", "SlicedImages")
|
||
self.sliced_files = []
|
||
|
||
# 创建界面
|
||
self.create_widgets()
|
||
|
||
def create_widgets(self):
|
||
# 主框架
|
||
main_frame = Frame(self.root, padx=20, pady=20)
|
||
main_frame.pack(fill="both", expand=True)
|
||
|
||
# 源图像选择
|
||
source_frame = Frame(main_frame, pady=10)
|
||
source_frame.pack(fill="x")
|
||
|
||
Label(source_frame, text="源图像:", font=("SimHei", 10, "bold")).pack(anchor="w")
|
||
|
||
source_path_frame = Frame(source_frame)
|
||
source_path_frame.pack(fill="x", pady=5)
|
||
|
||
self.source_entry = Entry(source_path_frame, width=50)
|
||
self.source_entry.pack(side="left", fill="x", expand=True, padx=5)
|
||
|
||
Button(source_path_frame, text="浏览", command=self.browse_source).pack(side="left", padx=5)
|
||
|
||
# 分割设置
|
||
settings_frame = Frame(main_frame, pady=10)
|
||
settings_frame.pack(fill="x")
|
||
|
||
Label(settings_frame, text="分割设置:", font=("SimHei", 10, "bold")).pack(anchor="w")
|
||
|
||
# 分割大小
|
||
size_frame = Frame(settings_frame)
|
||
size_frame.pack(fill="x", pady=5)
|
||
|
||
Label(size_frame, text="分割大小:").pack(side="left", padx=5)
|
||
self.size_entry = Entry(size_frame, width=10)
|
||
self.size_entry.insert(0, str(self.slice_size))
|
||
self.size_entry.pack(side="left", padx=5)
|
||
Label(size_frame, text="x").pack(side="left", padx=5)
|
||
self.size_entry.pack(side="left", padx=5)
|
||
|
||
# 命名设置
|
||
naming_frame = Frame(settings_frame)
|
||
naming_frame.pack(fill="x", pady=5)
|
||
|
||
Label(naming_frame, text="命名设置:", font=("SimHei", 9, "bold")).pack(anchor="w", padx=5)
|
||
|
||
# 前缀
|
||
prefix_frame = Frame(naming_frame)
|
||
prefix_frame.pack(fill="x", pady=2, padx=10)
|
||
Label(prefix_frame, text="前缀:", width=8).pack(side="left", padx=5)
|
||
self.prefix_entry = Entry(prefix_frame, width=20)
|
||
self.prefix_entry.insert(0, "slice")
|
||
self.prefix_entry.pack(side="left", padx=5)
|
||
|
||
# 后缀
|
||
suffix_frame = Frame(naming_frame)
|
||
suffix_frame.pack(fill="x", pady=2, padx=10)
|
||
Label(suffix_frame, text="后缀:", width=8).pack(side="left", padx=5)
|
||
self.suffix_entry = Entry(suffix_frame, width=20)
|
||
self.suffix_entry.insert(0, "")
|
||
self.suffix_entry.pack(side="left", padx=5)
|
||
|
||
# 排序方式
|
||
sort_frame = Frame(naming_frame)
|
||
sort_frame.pack(fill="x", pady=2, padx=10)
|
||
Label(sort_frame, text="排序方式:", width=8).pack(side="left", padx=5)
|
||
Frame(sort_frame).pack(side="left", padx=5)
|
||
Label(sort_frame, text="顺序编号 (从左到右,从上到下)").pack(side="left", padx=5)
|
||
self.sort_var = StringVar(value="sequential")
|
||
|
||
# 文件格式
|
||
format_frame = Frame(naming_frame)
|
||
format_frame.pack(fill="x", pady=2, padx=10)
|
||
Label(format_frame, text="文件格式:", width=8).pack(side="left", padx=5)
|
||
self.format_var = StringVar(value="png")
|
||
Frame(format_frame).pack(side="left", padx=5)
|
||
Radiobutton(format_frame, text="PNG", variable=self.format_var, value="png").pack(side="left", padx=5)
|
||
Radiobutton(format_frame, text="JPG", variable=self.format_var, value="jpg").pack(side="left", padx=5)
|
||
|
||
# 输出目录
|
||
output_frame = Frame(settings_frame)
|
||
output_frame.pack(fill="x", pady=5)
|
||
|
||
Label(output_frame, text="输出目录:").pack(side="left", padx=5)
|
||
self.output_entry = Entry(output_frame, width=40)
|
||
self.output_entry.insert(0, self.output_dir)
|
||
self.output_entry.pack(side="left", fill="x", expand=True, padx=5)
|
||
Button(output_frame, text="浏览", command=self.browse_output).pack(side="left", padx=5)
|
||
|
||
# 执行按钮
|
||
Button(main_frame, text="执行分割", command=self.slice_image, font=("SimHei", 12, "bold"), height=2, width=20).pack(pady=20)
|
||
|
||
# 结果显示
|
||
result_frame = Frame(main_frame, pady=10)
|
||
result_frame.pack(fill="both", expand=True)
|
||
|
||
Label(result_frame, text="分割结果:", font=("SimHei", 10, "bold")).pack(anchor="w")
|
||
|
||
# 结果列表
|
||
list_frame = Frame(result_frame)
|
||
list_frame.pack(fill="both", expand=True)
|
||
|
||
self.result_list = Listbox(list_frame, width=70, height=10)
|
||
self.result_list.pack(side="left", fill="both", expand=True)
|
||
|
||
# 垂直滚动条
|
||
vscrollbar = Scrollbar(list_frame, orient=VERTICAL, command=self.result_list.yview)
|
||
vscrollbar.pack(side="right", fill="y")
|
||
self.result_list.config(yscrollcommand=vscrollbar.set)
|
||
|
||
# 水平滚动条
|
||
hscrollbar = Scrollbar(list_frame, orient=HORIZONTAL, command=self.result_list.xview)
|
||
hscrollbar.pack(side="bottom", fill="x")
|
||
self.result_list.config(xscrollcommand=hscrollbar.set)
|
||
|
||
def browse_source(self):
|
||
"""浏览选择源图像"""
|
||
file_path = filedialog.askopenfilename(
|
||
title="选择源图像",
|
||
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.tiff *.gif"), ("All files", "*.*")]
|
||
)
|
||
if file_path:
|
||
self.source_entry.delete(0, END)
|
||
self.source_entry.insert(0, file_path)
|
||
self.source_image_path = file_path
|
||
|
||
def browse_output(self):
|
||
"""浏览选择输出目录"""
|
||
dir_path = filedialog.askdirectory(title="选择输出目录")
|
||
if dir_path:
|
||
self.output_entry.delete(0, END)
|
||
self.output_entry.insert(0, dir_path)
|
||
self.output_dir = dir_path
|
||
|
||
def process_slice(self, img, x, y, width, height, prefix, suffix, index, file_format):
|
||
"""处理单个图像切片"""
|
||
# 计算切割区域
|
||
start_x = x * self.slice_size
|
||
start_y = y * self.slice_size
|
||
end_x = min(start_x + self.slice_size, width)
|
||
end_y = min(start_y + self.slice_size, height)
|
||
|
||
# 切割图像
|
||
slice_img = img.crop((start_x, start_y, end_x, end_y))
|
||
|
||
# 生成文件名
|
||
parts = []
|
||
if prefix:
|
||
parts.append(prefix)
|
||
|
||
# 顺序编号:使用连续的数字
|
||
parts.append(f"{index-1}") # 从0开始
|
||
|
||
if suffix:
|
||
parts.append(suffix)
|
||
filename = "_".join(parts) + f".{file_format}"
|
||
|
||
# 保存切割后的图像
|
||
output_path = os.path.join(self.output_dir, filename)
|
||
if file_format == "jpg":
|
||
# 对于JPG格式,需要确保图像模式为RGB
|
||
if slice_img.mode == "RGBA":
|
||
# 创建白色背景
|
||
background = Image.new("RGB", slice_img.size, (255, 255, 255))
|
||
# 粘贴图像,使用alpha通道作为蒙版
|
||
background.paste(slice_img, mask=slice_img.split()[3])
|
||
slice_img = background
|
||
elif slice_img.mode != "RGB":
|
||
slice_img = slice_img.convert("RGB")
|
||
slice_img.save(output_path, "JPEG", quality=95)
|
||
else:
|
||
# 对于PNG格式,直接保存
|
||
slice_img.save(output_path, "PNG")
|
||
|
||
# 添加到结果列表
|
||
self.result_list.insert(END, output_path)
|
||
self.sliced_files.append(output_path)
|
||
|
||
def slice_image(self):
|
||
"""执行图像分割"""
|
||
# 获取输入
|
||
self.source_image_path = self.source_entry.get().strip()
|
||
self.output_dir = self.output_entry.get().strip()
|
||
|
||
try:
|
||
self.slice_size = int(self.size_entry.get().strip())
|
||
if self.slice_size <= 0:
|
||
raise ValueError("分割大小必须大于0")
|
||
except ValueError as e:
|
||
messagebox.showerror("错误", f"分割大小设置错误: {e}")
|
||
return
|
||
|
||
# 验证源图像
|
||
if not self.source_image_path:
|
||
messagebox.showerror("错误", "请选择源图像")
|
||
return
|
||
|
||
if not os.path.exists(self.source_image_path):
|
||
messagebox.showerror("错误", "源图像文件不存在")
|
||
return
|
||
|
||
# 创建输出目录
|
||
if not os.path.exists(self.output_dir):
|
||
try:
|
||
os.makedirs(self.output_dir)
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"无法创建输出目录: {e}")
|
||
return
|
||
|
||
try:
|
||
# 获取命名设置
|
||
prefix = self.prefix_entry.get().strip()
|
||
suffix = self.suffix_entry.get().strip()
|
||
sort_order = self.sort_var.get()
|
||
file_format = self.format_var.get().lower()
|
||
|
||
# 禁用PIL的解压炸弹安全限制
|
||
Image.MAX_IMAGE_PIXELS = None
|
||
|
||
# 打开图像
|
||
with Image.open(self.source_image_path) as img:
|
||
width, height = img.size
|
||
|
||
# 计算分割数量
|
||
x_count = (width + self.slice_size - 1) // self.slice_size
|
||
y_count = (height + self.slice_size - 1) // self.slice_size
|
||
total_slices = x_count * y_count
|
||
|
||
# 清空结果列表
|
||
self.result_list.delete(0, END)
|
||
self.sliced_files = []
|
||
|
||
# 执行分割
|
||
current = 0
|
||
|
||
# 行优先排序 (从上到下,从左到右) - 正确的拼接顺序
|
||
for y in range(y_count):
|
||
for x in range(x_count):
|
||
current += 1
|
||
self.process_slice(img, x, y, width, height, prefix, suffix, current, file_format)
|
||
|
||
# 显示完成消息
|
||
messagebox.showinfo("完成", f"图像分割完成!\n共生成 {total_slices} 个图像块\n保存路径: {self.output_dir}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"分割图像时出错: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def main():
|
||
"""主函数"""
|
||
root = Tk()
|
||
app = ImageSlicerApp(root)
|
||
root.mainloop()
|
||
|
||
if __name__ == "__main__":
|
||
main()
|