import os
import sys
import threading
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageOps, ImageFilter
class LineArtConverter:
def __init__(self, root):
self.root = root
self.root.title("图片转线稿工具-如影随行开发")
self.root.geometry("600x600")
self.root.resizable(True, True)
# 设置中文字体支持
self.setup_fonts()
# 存储要处理的文件路径
self.file_paths = []
# 创建UI
self.create_widgets()
# 进度相关变量
self.total_files = 0
self.processed_files = 0
def setup_fonts(self):
"""设置支持中文的字体"""
default_font = ('SimHei', 10)
self.root.option_add("*Font", default_font)
def create_widgets(self):
"""创建界面组件"""
# 创建主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text="图片转线稿工具", font=('SimHei', 16, 'bold'))
title_label.pack(pady=10)
# 说明文本
desc_label = ttk.Label(
main_frame,
text="该工具可以将照片转换为线稿风格。支持单张图片或整个文件夹的批量处理。",
wraplength=500
)
desc_label.pack(pady=5)
# 文件选择按钮区域
select_frame = ttk.Frame(main_frame)
select_frame.pack(fill=tk.X, pady=10)
import_img_btn = ttk.Button(select_frame, text="导入图片", command=self.import_images)
import_img_btn.pack(side=tk.LEFT, padx=5)
import_folder_btn = ttk.Button(select_frame, text="导入文件夹", command=self.import_folder)
import_folder_btn.pack(side=tk.LEFT, padx=5)
# 选择的文件列表
list_frame = ttk.LabelFrame(main_frame, text="已选择的文件", padding="5")
list_frame.pack(fill=tk.BOTH, expand=True, pady=10)
# 滚动条
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 文件列表
self.file_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, selectmode=tk.EXTENDED)
self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.file_listbox.yview)
# 移除选中文件按钮
remove_btn = ttk.Button(main_frame, text="移除选中文件", command=self.remove_selected)
remove_btn.pack(pady=5, anchor=tk.W)
# 进度条
self.progress_frame = ttk.LabelFrame(main_frame, text="处理进度", padding="5")
self.progress_frame.pack(fill=tk.X, pady=10)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(
self.progress_frame,
variable=self.progress_var,
maximum=100
)
self.progress_bar.pack(fill=tk.X, pady=5)
self.progress_label = ttk.Label(self.progress_frame, text="等待开始处理...")
self.progress_label.pack(anchor=tk.W)
# 按钮区域
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=10)
# 开始处理按钮
self.process_btn = ttk.Button(
btn_frame,
text="开始转线稿",
command=self.start_processing,
style='Accent.TButton'
)
self.process_btn.pack(side=tk.RIGHT)
# 样式设置
style = ttk.Style()
style.configure('Accent.TButton', font=('SimHei', 10, 'bold'))
def import_images(self):
"""导入单张或多张图片"""
file_types = (
('图像文件', '*.jpg *.jpeg *.png *.bmp *.gif'),
('所有文件', '*.*')
)
filenames = filedialog.askopenfilenames(
title="选择图片",
initialdir="/",
filetypes=file_types
)
if filenames:
for filename in filenames:
if filename not in self.file_paths:
self.file_paths.append(filename)
self.file_listbox.insert(tk.END, os.path.basename(filename))
def import_folder(self):
"""导入文件夹"""
folder = filedialog.askdirectory(title="选择图片文件夹")
if folder:
# 获取文件夹中所有图像文件
valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif')
image_files = [
f for f in os.listdir(folder)
if os.path.isfile(os.path.join(folder, f)) and
f.lower().endswith(valid_extensions)
]
if not image_files:
messagebox.showinfo("提示", f"在 {folder} 中未找到任何图像文件")
return
# 添加文件到列表
for file in image_files:
file_path = os.path.join(folder, file)
if file_path not in self.file_paths:
self.file_paths.append(file_path)
self.file_listbox.insert(tk.END, file)
messagebox.showinfo("提示", f"已添加 {len(image_files)} 个图像文件")
def remove_selected(self):
"""移除选中的文件"""
selected_indices = self.file_listbox.curselection()
if not selected_indices:
return
# 从后往前删除,避免索引变化问题
for i in sorted(selected_indices, reverse=True):
self.file_listbox.delete(i)
del self.file_paths[i]
def start_processing(self):
"""开始处理选中的文件"""
if not self.file_paths:
messagebox.showwarning("警告", "请先选择要处理的图片或文件夹")
return
# 禁用处理按钮和文件操作,防止重复处理
self.process_btn.config(state=tk.DISABLED)
# 初始化进度
self.total_files = len(self.file_paths)
self.processed_files = 0
self.progress_var.set(0)
self.progress_label.config(text=f"准备处理 {self.total_files} 个文件...")
# 在新线程中处理文件,避免界面卡死
processing_thread = threading.Thread(target=self.process_files)
processing_thread.daemon = True
processing_thread.start()
def update_progress(self, filename, success=True):
"""更新进度条和状态"""
self.processed_files += 1
progress = (self.processed_files / self.total_files) * 100
self.progress_var.set(progress)
status = "成功" if success else "失败"
self.progress_label.config(
text=f"已处理 {self.processed_files}/{self.total_files} 个文件 - {os.path.basename(filename)} {status}"
)
# 处理完成后恢复按钮状态
if self.processed_files == self.total_files:
self.progress_label.config(text=f"处理完成!共处理 {self.total_files} 个文件")
self.process_btn.config(state=tk.NORMAL)
messagebox.showinfo("完成", "所有文件处理完成!")
def process_files(self):
"""处理所有选中的文件"""
for file_path in self.file_paths:
try:
# 调用转换函数
result = self.convert_to_line_art(file_path)
# 在主线程中更新UI
self.root.after(0, self.update_progress, file_path, result is not None)
except Exception as e:
print(f"处理 {file_path} 时出错: {str(e)}")
self.root.after(0, self.update_progress, file_path, False)
def convert_to_line_art(self, image_path, radius=2):
"""
将照片转换为线稿
参数:
image_path: 原始图像路径
radius: 最小值滤镜的半径,默认为2像素
"""
try:
# 打开原始图像
original = Image.open(image_path).convert('RGB')
# 步骤1: 复制图层并去色(图层1)
layer1 = original.copy()
layer1 = ImageOps.grayscale(layer1) # 去色处理
# 步骤2: 复制图层1创建图层2并反相
layer2 = layer1.copy()
layer2 = ImageOps.invert(layer2) # 反相处理
# 步骤3: 对图层2应用最小值滤镜(模拟滤镜-其他-最小值)
layer2 = layer2.filter(ImageFilter.MinFilter(size=radius+1)) # size为半径+1
# 步骤4: 将图层2与图层1以线性减淡模式混合
# 实现线性减淡(添加)混合模式: result = min(layer1 + layer2, 255)
line_art = Image.new('L', layer1.size)
for x in range(layer1.width):
for y in range(layer1.height):
pixel1 = layer1.getpixel((x, y))
pixel2 = layer2.getpixel((x, y))
blended = min(pixel1 + pixel2, 255)
line_art.putpixel((x, y), blended)
# 构建输出文件路径
dir_name, file_name = os.path.split(image_path)
base_name, ext = os.path.splitext(file_name)
output_path = os.path.join(dir_name, f"{base_name}_线稿.jpg")
# 保存结果
line_art.save(output_path)
return output_path
except Exception as e:
print(f"处理图像时出错: {str(e)}")
return None
if __name__ == "__main__":
root = tk.Tk()
app = LineArtConverter(root)
root.mainloop()