app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import os
  2. import json
  3. import threading
  4. import tkinter as tk
  5. from tkinter import ttk, filedialog, messagebox
  6. from queue import Queue
  7. from PIL import Image, ImageTk
  8. import numpy as np
  9. class ImageProcessorApp:
  10. def __init__(self, root):
  11. self.root = root
  12. self.root.title("智能图片合成器 (带断点续传)")
  13. self.root.geometry("800x600")
  14. # 初始化状态
  15. self.is_running = False
  16. self.pause_flag = False
  17. self.status_file = "progress_status.json"
  18. self.task_queue = Queue()
  19. self.current_task = {"bg_index": 0, "input_index": 0}
  20. # 边距设置(可配置)
  21. self.margins = self.load_margins() or {"top": 76, "bottom": 76, "left": 57, "right": 57}
  22. # 创建界面
  23. self.create_widgets()
  24. self.load_progress()
  25. def create_widgets(self):
  26. # 控制面板
  27. control_frame = ttk.Frame(self.root)
  28. control_frame.pack(fill=tk.X, padx=10, pady=5)
  29. self.start_btn = ttk.Button(control_frame, text="开始", command=self.start_processing)
  30. self.pause_btn = ttk.Button(control_frame, text="暂停", state=tk.DISABLED, command=self.toggle_pause)
  31. self.reset_btn = ttk.Button(control_frame, text="重置", command=self.reset_progress)
  32. self.start_btn.pack(side=tk.LEFT, padx=5)
  33. self.pause_btn.pack(side=tk.LEFT, padx=5)
  34. self.reset_btn.pack(side=tk.LEFT, padx=5)
  35. # 文件夹选择
  36. self.setup_folder_controls()
  37. # 边距配置
  38. self.setup_margin_controls()
  39. # 进度显示
  40. self.progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL)
  41. self.progress.pack(fill=tk.X, padx=10, pady=5)
  42. self.status_label = ttk.Label(self.root, text="就绪")
  43. self.status_label.pack()
  44. # 背景图片尺寸显示
  45. self.bg_dimensions_label = ttk.Label(self.root, text="背景尺寸: 未加载")
  46. self.bg_dimensions_label.pack()
  47. def setup_folder_controls(self):
  48. frame = ttk.LabelFrame(self.root, text="文件夹设置")
  49. frame.pack(fill=tk.X, padx=10, pady=5)
  50. # 输入文件夹
  51. ttk.Label(frame, text="输入:").grid(row=0, column=0, padx=5)
  52. self.input_dir = ttk.Entry(frame, width=40)
  53. self.input_dir.grid(row=0, column=1, padx=5)
  54. ttk.Button(frame, text="浏览", command=lambda: self.select_dir(self.input_dir)).grid(row=0, column=2)
  55. # 背景文件夹
  56. ttk.Label(frame, text="背景:").grid(row=1, column=0, padx=5)
  57. self.bg_dir = ttk.Entry(frame, width=40)
  58. self.bg_dir.grid(row=1, column=1, padx=5)
  59. ttk.Button(frame, text="浏览", command=lambda: self.select_dir(self.bg_dir)).grid(row=1, column=2)
  60. # 输出文件夹
  61. ttk.Label(frame, text="输出:").grid(row=2, column=0, padx=5)
  62. self.output_dir = ttk.Entry(frame, width=40)
  63. self.output_dir.grid(row=2, column=1, padx=5)
  64. ttk.Button(frame, text="浏览", command=lambda: self.select_dir(self.output_dir)).grid(row=2, column=2)
  65. def setup_margin_controls(self):
  66. frame = ttk.LabelFrame(self.root, text="边距设置")
  67. frame.pack(fill=tk.X, padx=10, pady=5)
  68. ttk.Label(frame, text="上:").grid(row=0, column=0, padx=5)
  69. self.top_margin = ttk.Entry(frame, width=10)
  70. self.top_margin.insert(0, str(self.margins["top"]))
  71. self.top_margin.grid(row=0, column=1, padx=5)
  72. ttk.Label(frame, text="下:").grid(row=0, column=2, padx=5)
  73. self.bottom_margin = ttk.Entry(frame, width=10)
  74. self.bottom_margin.insert(0, str(self.margins["bottom"]))
  75. self.bottom_margin.grid(row=0, column=3, padx=5)
  76. ttk.Label(frame, text="左:").grid(row=1, column=0, padx=5)
  77. self.left_margin = ttk.Entry(frame, width=10)
  78. self.left_margin.insert(0, str(self.margins["left"]))
  79. self.left_margin.grid(row=1, column=1, padx=5)
  80. ttk.Label(frame, text="右:").grid(row=1, column=2, padx=5)
  81. self.right_margin = ttk.Entry(frame, width=10)
  82. self.right_margin.insert(0, str(self.margins["right"]))
  83. self.right_margin.grid(row=1, column=3, padx=5)
  84. ttk.Button(frame, text="应用", command=self.update_margins).grid(row=1, column=4, padx=5)
  85. def update_margins(self):
  86. try:
  87. self.margins = {
  88. "top": int(self.top_margin.get()),
  89. "bottom": int(self.bottom_margin.get()),
  90. "left": int(self.left_margin.get()),
  91. "right": int(self.right_margin.get()),
  92. }
  93. messagebox.showinfo("成功", "边距已更新!")
  94. except ValueError:
  95. messagebox.showerror("错误", "请输入有效的数字!")
  96. def load_margins(self):
  97. if os.path.exists("margins.json"):
  98. try:
  99. with open("margins.json", "r") as f:
  100. return json.load(f)
  101. except:
  102. pass
  103. return None
  104. def save_margins(self):
  105. with open("margins.json", "w") as f:
  106. json.dump(self.margins, f)
  107. def select_dir(self, entry):
  108. path = filedialog.askdirectory()
  109. if path:
  110. entry.delete(0, tk.END)
  111. entry.insert(0, path)
  112. def load_progress(self):
  113. if os.path.exists(self.status_file):
  114. try:
  115. with open(self.status_file, 'r') as f:
  116. data = json.load(f)
  117. self.current_task = data.get('current_task', self.current_task)
  118. self.input_dir.insert(0, data.get('input_folder', ''))
  119. self.bg_dir.insert(0, data.get('bg_folder', ''))
  120. self.output_dir.insert(0, data.get('output_folder', ''))
  121. self.progress["value"] = data.get('progress', 0)
  122. except:
  123. pass
  124. def save_progress(self):
  125. data = {
  126. "input_folder": self.input_dir.get(),
  127. "bg_folder": self.bg_dir.get(),
  128. "output_folder": self.output_dir.get(),
  129. "current_task": self.current_task,
  130. "progress": self.progress["value"]
  131. }
  132. with open(self.status_file, 'w') as f:
  133. json.dump(data, f)
  134. def start_processing(self):
  135. if not self.validate_paths():
  136. return
  137. self.is_running = True
  138. self.pause_flag = False
  139. self.start_btn.config(state=tk.DISABLED)
  140. self.pause_btn.config(state=tk.NORMAL)
  141. # 初始化任务队列
  142. bg_images = self.get_image_files(self.bg_dir.get())
  143. input_images = self.get_image_files(self.input_dir.get())
  144. total = len(bg_images) * len(input_images)
  145. self.progress["maximum"] = total
  146. # 创建处理线程
  147. self.worker_thread = threading.Thread(
  148. target=self.process_images,
  149. args=(bg_images, input_images),
  150. daemon=True
  151. )
  152. self.worker_thread.start()
  153. self.monitor_thread()
  154. def toggle_pause(self):
  155. self.pause_flag = not self.pause_flag
  156. self.pause_btn.config(text="继续" if self.pause_flag else "暂停")
  157. self.save_progress()
  158. def reset_progress(self):
  159. self.progress["value"] = 0
  160. self.current_task = {"bg_index": 0, "input_index": 0}
  161. if os.path.exists(self.status_file):
  162. os.remove(self.status_file)
  163. def validate_paths(self):
  164. required = [
  165. (self.input_dir.get(), "请输入输入文件夹"),
  166. (self.bg_dir.get(), "请输入背景文件夹"),
  167. (self.output_dir.get(), "请指定输出文件夹")
  168. ]
  169. for path, msg in required:
  170. if not path:
  171. messagebox.showerror("错误", msg)
  172. return False
  173. if not os.path.exists(path):
  174. messagebox.showerror("错误", f"路径不存在: {path}")
  175. return False
  176. return True
  177. def monitor_thread(self):
  178. if self.worker_thread.is_alive():
  179. self.root.after(100, self.monitor_thread)
  180. else:
  181. self.start_btn.config(state=tk.NORMAL)
  182. self.pause_btn.config(state=tk.DISABLED)
  183. self.is_running = False
  184. self.save_progress()
  185. def process_images(self, bg_images, input_images):
  186. bg_path = self.bg_dir.get()
  187. input_path = self.input_dir.get()
  188. output_path = self.output_dir.get()
  189. for bg_idx in range(self.current_task["bg_index"], len(bg_images)):
  190. if not self.is_running or self.pause_flag:
  191. break
  192. bg_name = bg_images[bg_idx]
  193. try:
  194. with Image.open(os.path.join(bg_path, bg_name)) as img:
  195. bg_img = self.process_background(img)
  196. # 更新背景图片尺寸显示
  197. self.root.after(0, lambda: self.bg_dimensions_label.config(text=f"背景尺寸: {bg_img.size[0]}x{bg_img.size[1]}"))
  198. except Exception as e:
  199. continue
  200. target_size = self.calculate_target_size(bg_img.size)
  201. subfolder = self.create_subfolder(output_path, bg_name)
  202. for input_idx in range(self.current_task["input_index"], len(input_images)):
  203. if not self.is_running or self.pause_flag:
  204. self.current_task.update({"bg_index": bg_idx, "input_index": input_idx})
  205. break
  206. input_name = input_images[input_idx]
  207. try:
  208. self.process_single_image(
  209. os.path.join(input_path, input_name),
  210. bg_img,
  211. target_size,
  212. os.path.join(subfolder, f"composite_{input_name}")
  213. )
  214. self.progress["value"] += 1
  215. self.status_label.config(text=f"处理中: {self.progress['value']}/{self.progress['maximum']}")
  216. except Exception as e:
  217. continue
  218. self.current_task["input_index"] = input_idx + 1
  219. self.save_progress()
  220. self.current_task.update({"bg_index": bg_idx + 1, "input_index": 0})
  221. self.save_progress()
  222. def process_background(self, img):
  223. processed = img.copy().convert("RGBA")
  224. data = np.array(processed)
  225. r, g, b, a = data.T
  226. white_mask = (r > 240) & (g > 240) & (b > 240)
  227. data[..., :][white_mask.T] = (255, 255, 255, 0)
  228. return Image.fromarray(data)
  229. def calculate_target_size(self, bg_size):
  230. return (
  231. bg_size[0] - self.margins["left"] - self.margins["right"],
  232. bg_size[1] - self.margins["top"] - self.margins["bottom"]
  233. )
  234. def create_subfolder(self, output_path, bg_name):
  235. subfolder = os.path.join(output_path, os.path.splitext(bg_name)[0])
  236. os.makedirs(subfolder, exist_ok=True)
  237. return subfolder
  238. def process_single_image(self, input_path, bg_img, target_size, output_path):
  239. with Image.open(input_path) as img:
  240. input_img = img.convert('RGBA')
  241. resized = self.adaptive_resize(input_img, target_size)
  242. composite = Image.new('RGBA', bg_img.size)
  243. composite.paste(bg_img, (0, 0))
  244. composite.paste(resized, (self.margins["left"], self.margins["top"]), mask=resized)
  245. composite.save(output_path)
  246. @staticmethod
  247. def adaptive_resize(img, target_size):
  248. width, height = img.size
  249. target_w, target_h = target_size
  250. ratio = min(target_w / width, target_h / height)
  251. new_size = (int(width * ratio), int(height * ratio))
  252. resized = img.resize(new_size, Image.LANCZOS)
  253. canvas = Image.new("RGBA", target_size, (0, 0, 0, 0))
  254. canvas.paste(resized, (
  255. (target_w - new_size[0]) // 2,
  256. (target_h - new_size[1]) // 2
  257. ))
  258. return canvas
  259. @staticmethod
  260. def get_image_files(folder):
  261. return [f for f in os.listdir(folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif'))]
  262. if __name__ == "__main__":
  263. root = tk.Tk()
  264. app = ImageProcessorApp(root)
  265. root.mainloop()