image_classifier.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox
  3. from PIL import Image, ImageTk, ImageEnhance
  4. import os
  5. import shutil
  6. from pathlib import Path
  7. import numpy as np
  8. from collections import Counter
  9. import cv2
  10. class ImageClassifier:
  11. def __init__(self, root):
  12. self.root = root
  13. self.root.title("图片分类器")
  14. self.root.geometry("1200x800")
  15. # 当前图片路径
  16. self.current_image_path = None
  17. # 当前文件夹路径
  18. self.current_folder_path = None
  19. # 图片列表
  20. self.image_list = []
  21. # 当前图片索引
  22. self.current_index = 0
  23. # 图片处理参数
  24. self.zoom_factor = 1.0
  25. self.rotation_angle = 0
  26. self.brightness_factor = 1.0
  27. # 天空检测参数
  28. self.sky_threshold = 0.7 # 天空占比阈值
  29. self.blue_threshold = 0.6 # 蓝色占比阈值
  30. # 操作历史记录
  31. self.operation_history = [] # 存储操作历史
  32. self.setup_ui()
  33. self.setup_keyboard_shortcuts()
  34. def setup_ui(self):
  35. # 创建主框架
  36. main_frame = ttk.Frame(self.root)
  37. main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
  38. # 左侧控制面板
  39. control_frame = ttk.Frame(main_frame)
  40. control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5)
  41. # 选择文件夹按钮
  42. ttk.Button(control_frame, text="选择文件夹 (F)", command=self.select_folder).pack(pady=5)
  43. # 分类按钮
  44. ttk.Button(control_frame, text="有无人机 (1)", command=lambda: self.classify_image("drone")).pack(pady=5)
  45. ttk.Button(control_frame, text="有鸟类 (2)", command=lambda: self.classify_image("bird")).pack(pady=5)
  46. ttk.Button(control_frame, text="有鸟类和无人机 (3)", command=lambda: self.classify_image("both")).pack(pady=5)
  47. ttk.Button(control_frame, text="无目标 (4)", command=lambda: self.classify_image("none")).pack(pady=5)
  48. # 导航按钮
  49. nav_frame = ttk.Frame(control_frame)
  50. nav_frame.pack(pady=10)
  51. ttk.Button(nav_frame, text="上一张 (←)", command=self.prev_image).pack(side=tk.LEFT, padx=5)
  52. ttk.Button(nav_frame, text="下一张 (→)", command=self.next_image).pack(side=tk.LEFT, padx=5)
  53. # 撤回按钮
  54. ttk.Button(control_frame, text="撤回操作 (Z)", command=self.undo_operation).pack(pady=5)
  55. # 图片处理控制
  56. process_frame = ttk.LabelFrame(control_frame, text="图片处理")
  57. process_frame.pack(pady=10, fill=tk.X)
  58. # 缩放控制
  59. ttk.Label(process_frame, text="缩放:").pack()
  60. self.zoom_scale = ttk.Scale(process_frame, from_=0.1, to=3.0, orient=tk.HORIZONTAL,
  61. command=self.update_zoom)
  62. self.zoom_scale.set(1.0)
  63. self.zoom_scale.pack(fill=tk.X, padx=5)
  64. # 旋转控制
  65. ttk.Label(process_frame, text="旋转:").pack()
  66. self.rotation_scale = ttk.Scale(process_frame, from_=0, to=360, orient=tk.HORIZONTAL,
  67. command=self.update_rotation)
  68. self.rotation_scale.set(0)
  69. self.rotation_scale.pack(fill=tk.X, padx=5)
  70. # 亮度控制
  71. ttk.Label(process_frame, text="亮度:").pack()
  72. self.brightness_scale = ttk.Scale(process_frame, from_=0.1, to=2.0, orient=tk.HORIZONTAL,
  73. command=self.update_brightness)
  74. self.brightness_scale.set(1.0)
  75. self.brightness_scale.pack(fill=tk.X, padx=5)
  76. # 天空检测
  77. ttk.Label(process_frame, text="天空检测:").pack()
  78. self.auto_detect_var = tk.BooleanVar(value=False)
  79. ttk.Checkbutton(process_frame, text="自动检测天空图片",
  80. variable=self.auto_detect_var,
  81. command=self.toggle_auto_detect).pack()
  82. # 天空阈值控制
  83. ttk.Label(process_frame, text="天空阈值:").pack()
  84. self.threshold_scale = ttk.Scale(process_frame, from_=0.5, to=0.95, orient=tk.HORIZONTAL,
  85. command=self.update_threshold)
  86. self.threshold_scale.set(0.7)
  87. self.threshold_scale.pack(fill=tk.X, padx=5)
  88. # 重置按钮
  89. ttk.Button(process_frame, text="重置图片 (R)", command=self.reset_image).pack(pady=5)
  90. # 右侧图片显示区域
  91. self.image_frame = ttk.Frame(main_frame)
  92. self.image_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
  93. # 图片标签
  94. self.image_label = ttk.Label(self.image_frame)
  95. self.image_label.pack(fill=tk.BOTH, expand=True)
  96. # 状态标签
  97. self.status_label = ttk.Label(self.root, text="请选择文件夹")
  98. self.status_label.pack(side=tk.BOTTOM, pady=5)
  99. # 添加图片信息显示
  100. self.info_label = ttk.Label(self.root, text="")
  101. self.info_label.pack(side=tk.BOTTOM, pady=5)
  102. def setup_keyboard_shortcuts(self):
  103. self.root.bind('<F5>', lambda e: self.select_folder())
  104. self.root.bind('<Left>', lambda e: self.prev_image())
  105. self.root.bind('<Right>', lambda e: self.next_image())
  106. self.root.bind('1', lambda e: self.classify_image("drone"))
  107. self.root.bind('2', lambda e: self.classify_image("bird"))
  108. self.root.bind('3', lambda e: self.classify_image("both"))
  109. self.root.bind('4', lambda e: self.classify_image("none"))
  110. self.root.bind('r', lambda e: self.reset_image())
  111. self.root.bind('z', lambda e: self.undo_operation())
  112. def update_zoom(self, value):
  113. self.zoom_factor = float(value)
  114. self.show_current_image()
  115. def update_rotation(self, value):
  116. self.rotation_angle = float(value)
  117. self.show_current_image()
  118. def update_brightness(self, value):
  119. self.brightness_factor = float(value)
  120. self.show_current_image()
  121. def reset_image(self):
  122. self.zoom_factor = 1.0
  123. self.rotation_angle = 0
  124. self.brightness_factor = 1.0
  125. self.zoom_scale.set(1.0)
  126. self.rotation_scale.set(0)
  127. self.brightness_scale.set(1.0)
  128. self.show_current_image()
  129. def select_folder(self):
  130. folder_path = filedialog.askdirectory()
  131. if folder_path:
  132. self.current_folder_path = folder_path
  133. self.load_images()
  134. def load_images(self):
  135. self.image_list = []
  136. for file in os.listdir(self.current_folder_path):
  137. if file.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
  138. self.image_list.append(os.path.join(self.current_folder_path, file))
  139. if self.image_list:
  140. self.current_index = 0
  141. self.show_current_image()
  142. self.status_label.config(text=f"已加载 {len(self.image_list)} 张图片")
  143. else:
  144. messagebox.showinfo("提示", "所选文件夹中没有图片")
  145. def show_current_image(self):
  146. if 0 <= self.current_index < len(self.image_list):
  147. self.current_image_path = self.image_list[self.current_index]
  148. # 打开图片
  149. image = Image.open(self.current_image_path)
  150. # 如果启用了自动检测,检查是否为天空图片
  151. if self.auto_detect_var.get():
  152. if self.is_sky_image(image):
  153. self.classify_image("none")
  154. return
  155. # 应用亮度调整
  156. enhancer = ImageEnhance.Brightness(image)
  157. image = enhancer.enhance(self.brightness_factor)
  158. # 应用旋转
  159. image = image.rotate(self.rotation_angle, expand=True)
  160. # 计算调整后的尺寸,保持宽高比
  161. width, height = image.size
  162. max_size = (800, 600)
  163. ratio = min(max_size[0]/width, max_size[1]/height)
  164. new_size = (int(width*ratio*self.zoom_factor), int(height*ratio*self.zoom_factor))
  165. image = image.resize(new_size, Image.Resampling.LANCZOS)
  166. # 转换为PhotoImage
  167. photo = ImageTk.PhotoImage(image)
  168. # 更新图片显示
  169. self.image_label.config(image=photo)
  170. self.image_label.image = photo # 保持引用
  171. # 更新状态
  172. self.status_label.config(text=f"图片 {self.current_index + 1}/{len(self.image_list)}")
  173. # 显示图片信息
  174. file_size = os.path.getsize(self.current_image_path) / 1024 # 转换为KB
  175. self.info_label.config(text=f"文件名: {os.path.basename(self.current_image_path)} | 尺寸: {width}x{height} | 大小: {file_size:.1f}KB")
  176. def next_image(self):
  177. if self.image_list:
  178. self.current_index = (self.current_index + 1) % len(self.image_list)
  179. self.show_current_image()
  180. def prev_image(self):
  181. if self.image_list:
  182. self.current_index = (self.current_index - 1) % len(self.image_list)
  183. self.show_current_image()
  184. def classify_image(self, category):
  185. if not self.current_image_path:
  186. return
  187. # 创建分类文件夹
  188. category_folders = {
  189. "drone": "有无人机",
  190. "bird": "有鸟类",
  191. "both": "有鸟类和无人机",
  192. "none": "无目标"
  193. }
  194. target_folder = os.path.join(self.current_folder_path, category_folders[category])
  195. os.makedirs(target_folder, exist_ok=True)
  196. # 移动图片到对应文件夹
  197. filename = os.path.basename(self.current_image_path)
  198. target_path = os.path.join(target_folder, filename)
  199. try:
  200. # 记录操作历史(在移动之前)
  201. operation_record = {
  202. 'action': 'classify',
  203. 'original_path': self.current_image_path,
  204. 'target_path': target_path,
  205. 'category': category,
  206. 'index': self.current_index,
  207. 'filename': filename
  208. }
  209. shutil.move(self.current_image_path, target_path)
  210. # 添加到操作历史
  211. self.operation_history.append(operation_record)
  212. # 限制历史记录数量(最多保留50条)
  213. if len(self.operation_history) > 50:
  214. self.operation_history.pop(0)
  215. self.image_list.pop(self.current_index)
  216. if self.image_list:
  217. self.current_index = self.current_index % len(self.image_list)
  218. self.show_current_image()
  219. else:
  220. self.image_label.config(image='')
  221. self.status_label.config(text="所有图片已分类完成")
  222. self.info_label.config(text="")
  223. except Exception as e:
  224. messagebox.showerror("错误", f"移动文件时出错:{str(e)}")
  225. def is_sky_image(self, image):
  226. # 转换为OpenCV格式
  227. img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
  228. # 转换为HSV颜色空间
  229. hsv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2HSV)
  230. # 定义天空的HSV范围(包括蓝色和灰色)
  231. # 蓝色天空范围
  232. lower_blue = np.array([100, 50, 50])
  233. upper_blue = np.array([130, 255, 255])
  234. # 灰色天空范围
  235. lower_gray = np.array([0, 0, 50])
  236. upper_gray = np.array([180, 30, 200])
  237. # 创建掩码
  238. mask_blue = cv2.inRange(hsv, lower_blue, upper_blue)
  239. mask_gray = cv2.inRange(hsv, lower_gray, upper_gray)
  240. # 合并掩码
  241. mask = cv2.bitwise_or(mask_blue, mask_gray)
  242. # 计算天空像素占比
  243. sky_ratio = np.sum(mask > 0) / (mask.shape[0] * mask.shape[1])
  244. # 计算图片上半部分的天空占比
  245. upper_half = mask[:mask.shape[0]//2, :]
  246. upper_sky_ratio = np.sum(upper_half > 0) / (upper_half.shape[0] * upper_half.shape[1])
  247. # 计算图片的纹理特征
  248. gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
  249. texture = cv2.Laplacian(gray, cv2.CV_64F).var()
  250. # 判断是否为天空图片
  251. # 1. 整体天空占比超过阈值
  252. # 2. 上半部分主要是天空
  253. # 3. 纹理变化较小(天空通常比较均匀)
  254. return (sky_ratio > self.sky_threshold and
  255. upper_sky_ratio > self.blue_threshold and
  256. texture < 500) # 纹理阈值可以根据需要调整
  257. def toggle_auto_detect(self):
  258. if self.auto_detect_var.get() and self.current_image_path:
  259. if self.is_sky_image(Image.open(self.current_image_path)):
  260. self.classify_image("none")
  261. messagebox.showinfo("提示", "检测到天空图片,已自动分类为'无目标'")
  262. def update_threshold(self, value):
  263. self.sky_threshold = float(value)
  264. def undo_operation(self):
  265. """撤回最后一次分类操作"""
  266. if not self.operation_history:
  267. messagebox.showinfo("提示", "没有可撤回的操作")
  268. return
  269. # 获取最后一次操作
  270. last_operation = self.operation_history.pop()
  271. if last_operation['action'] == 'classify':
  272. try:
  273. # 将文件移回原位置
  274. shutil.move(last_operation['target_path'], last_operation['original_path'])
  275. # 重新加载图片列表
  276. self.load_images()
  277. # 尝试定位到撤回的图片
  278. try:
  279. restored_index = self.image_list.index(last_operation['original_path'])
  280. self.current_index = restored_index
  281. except ValueError:
  282. # 如果找不到,就显示第一张
  283. self.current_index = 0
  284. if self.image_list:
  285. self.show_current_image()
  286. messagebox.showinfo("成功", f"已撤回对 '{last_operation['filename']}' 的分类操作")
  287. else:
  288. messagebox.showinfo("提示", "撤回成功,但当前文件夹中没有图片")
  289. except Exception as e:
  290. messagebox.showerror("错误", f"撤回操作失败:{str(e)}")
  291. # 如果撤回失败,将操作重新加入历史记录
  292. self.operation_history.append(last_operation)
  293. if __name__ == "__main__":
  294. root = tk.Tk()
  295. app = ImageClassifier(root)
  296. root.mainloop()