""" 岩屑图像切片工具 - 四点定位切分 按范围设置d值 """ import os import tkinter as tk from tkinter import filedialog, messagebox import pandas as pd from PIL import Image, ImageDraw, ImageTk class SliceToolApp: def __init__(self, root): self.root = root self.root.title("Rock Image Slice Tool") self.root.geometry("1500x950") # 变量 self.current_image_path = None self.current_image = None self.output_dir = tk.StringVar(value="./output") self.slice_size = tk.IntVar(value=256) # 4个角点 self.corner_points = [] self.temp_marker_ids = [] # d值范围设置: [{'start': 1, 'end': 101, 'd': 1.0}, ...] (左闭右开,包含1-100) self.dragging_point = None self.preview_line_ids = [] self.d_ranges = [{'start': 1, 'end': 101, 'd': 1.0}] # 100个格子的深度数据 self.depth_data = [] for r in range(10): row_data = [] for c in range(10): row_data.append(0) self.depth_data.append(row_data) # Canvas self.canvas = None self.scale = 1.0 self.offset_x = 0 self.offset_y = 0 self.display_w = 0 self.display_h = 0 self.setup_ui() self.calculate_all_depths() def setup_ui(self): # 标题 title_frame = tk.Frame(self.root, bg="#2c3e50", height=50) title_frame.pack(fill=tk.X) title_frame.pack_propagate(False) tk.Label(title_frame, text="Rock Image Slice Tool - Set d by Range", font=("Arial", 14, "bold"), bg="#2c3e50", fg="white").pack(pady=12) # 主容器 main_container = tk.PanedWindow(self.root, orient=tk.HORIZONTAL) main_container.pack(fill=tk.BOTH, expand=True) # 左侧 left_frame = tk.Frame(main_container, width=1000) main_container.add(left_frame, width=1000) self.canvas = tk.Canvas(left_frame, bg="#1a1a2e", width=1000, height=850, cursor="crosshair") self.canvas.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.canvas.bind("", self.on_canvas_click) # 按钮行 btn_frame = tk.Frame(left_frame) btn_frame.pack(fill=tk.X, padx=5, pady=5) tk.Button(btn_frame, text="Load Image", command=self.select_image, bg="#3498db", fg="white", font=("Arial", 10)).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame, text="Set Output", command=self.select_output_dir, bg="#34495e", fg="white", font=("Arial", 10)).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame, text="Clear Points", command=self.clear_points, bg="#e74c3c", fg="white", font=("Arial", 10)).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame, text="Execute Slice", command=self.execute_slice, bg="#27ae60", fg="white", font=("Arial", 10)).pack(side=tk.LEFT, padx=3) self.image_info_label = tk.Label(left_frame, text="No image loaded", bg="#34495e", fg="#3498db", font=("Arial", 9), anchor=tk.W) self.image_info_label.pack(fill=tk.X, padx=5, pady=2) # 右侧 right_frame = tk.Frame(main_container, bg="#2c3e50", width=400) main_container.add(right_frame, width=400) # 角点进度 progress_group = tk.LabelFrame(right_frame, text="Corner Points", font=("Arial", 11), bg="#34495e", fg="#3498db", padx=10, pady=10) progress_group.pack(fill=tk.X, padx=10, pady=5) self.progress_label = tk.Label(progress_group, text="Click 4 corners: 0/4", bg="#2c3e50", fg="#f1c40f", font=("Arial", 12, "bold"), anchor=tk.W) self.progress_label.pack(fill=tk.X) tk.Label(progress_group, text="任意顺序点击4个点,自动验证平行四边形", bg="#34495e", fg="#95a5a6", font=("Arial", 9)).pack(anchor=tk.W) # d值范围设置 range_group = tk.LabelFrame(right_frame, text="Set d by Range (Cell Range)", font=("Arial", 11), bg="#34495e", fg="#27ae60", padx=10, pady=10) range_group.pack(fill=tk.X, padx=10, pady=5) tk.Label(range_group, text="Cell#: Start ~ End | d value", bg="#34495e", fg="#ecf0f1", font=("Arial", 9)).pack(anchor=tk.W) # 范围输入框 self.range_frames = [] self.range_start_entries = [] self.range_end_entries = [] self.range_d_entries = [] for i, r in enumerate(self.d_ranges): frame = tk.Frame(range_group, bg="#34495e") frame.pack(fill=tk.X, pady=2) start_entry = tk.Entry(frame, width=6, font=("Arial", 9), justify=tk.CENTER) start_entry.pack(side=tk.LEFT, padx=2) start_entry.insert(0, str(r['start'])) tk.Label(frame, text="~", bg="#34495e", fg="white").pack(side=tk.LEFT) end_entry = tk.Entry(frame, width=6, font=("Arial", 9), justify=tk.CENTER) end_entry.pack(side=tk.LEFT, padx=2) end_entry.insert(0, str(r['end'] if r['end'] <= 100 else 101)) tk.Label(frame, text="d=", bg="#34495e", fg="white").pack(side=tk.LEFT, padx=(10,2)) d_entry = tk.Entry(frame, width=8, font=("Arial", 9), justify=tk.CENTER) d_entry.pack(side=tk.LEFT, padx=2) d_entry.insert(0, str(r['d'])) self.range_frames.append(frame) self.range_start_entries.append(start_entry) self.range_end_entries.append(end_entry) self.range_d_entries.append(d_entry) # 按钮行 range_btn_frame = tk.Frame(range_group, bg="#34495e") range_btn_frame.pack(fill=tk.X, pady=5) tk.Button(range_btn_frame, text="+ Add Range", command=self.add_range, bg="#3498db", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=2) tk.Button(range_btn_frame, text="- Remove", command=self.remove_range, bg="#e74c3c", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=2) tk.Button(range_btn_frame, text="Calculate", command=self.calculate_all_depths, bg="#27ae60", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=2) # 起始深度 start_depth_frame = tk.Frame(range_group, bg="#34495e") start_depth_frame.pack(fill=tk.X, pady=5) tk.Label(start_depth_frame, text="Start Depth:", bg="#34495e", fg="#ecf0f1", font=("Arial", 9)).pack(side=tk.LEFT) self.base_depth_entry = tk.Entry(start_depth_frame, width=10, font=("Arial", 9)) self.base_depth_entry.pack(side=tk.LEFT, padx=5) self.base_depth_entry.insert(0, "3660") tk.Button(start_depth_frame, text="Apply", command=self.calculate_all_depths, bg="#3498db", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=2) # 100格子表格 table_group = tk.LabelFrame(right_frame, text="100 Grid (拖选设置d值)", font=("Arial", 11), bg="#34495e", fg="#27ae60", padx=10, pady=5) table_group.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) table_container = tk.Frame(table_group, bg="#34495e") table_container.pack(fill=tk.BOTH, expand=True, pady=5) tk.Label(table_container, text="R\\C", bg="#2c3e50", fg="white", font=("Arial", 7, "bold"), width=4, relief=tk.RIDGE).grid(row=0, column=0, padx=1, pady=1) for c in range(10): tk.Label(table_container, text=str(c+1), bg="#3498db", fg="white", font=("Arial", 7, "bold"), width=5, relief=tk.RIDGE).grid(row=0, column=c+1, padx=1, pady=1) self.cell_labels = [] # 单元格标签 for r in range(10): tk.Label(table_container, text=str(r+1), bg="#3498db", fg="white", font=("Arial", 7, "bold"), width=4, relief=tk.RIDGE).grid(row=r+1, column=0, padx=1, pady=1) row_labels = [] for c in range(10): lbl = tk.Label(table_container, width=5, relief=tk.RIDGE, bg="#1a1a2e", fg="#ecf0f1", font=("Arial", 7), cursor="hand1") lbl.grid(row=r+1, column=c+1, padx=1, pady=1) lbl.bind('', lambda e, r=r, c=c: self.on_cell_select_start(r, c)) lbl.bind('', lambda e, r=r, c=c: self.on_cell_select_drag(r, c)) lbl.bind('', lambda e: self.on_cell_select_end()) row_labels.append(lbl) self.cell_labels.append(row_labels) # d值设置区域 d_frame = tk.Frame(table_group, bg="#34495e") d_frame.pack(fill=tk.X, pady=5) tk.Label(d_frame, text="d值:", bg="#34495e", fg="#ecf0f1", font=("Arial", 9)).pack(side=tk.LEFT) self.selected_d_entry = tk.Entry(d_frame, width=8, font=("Arial", 9), justify=tk.CENTER) self.selected_d_entry.pack(side=tk.LEFT, padx=3) self.selected_d_entry.insert(0, "1.0") tk.Button(d_frame, text="应用", command=self.apply_selected_d, bg="#27ae60", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=3) tk.Button(d_frame, text="全部", command=self.apply_all_d, bg="#3498db", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=3) tk.Button(d_frame, text="清除", command=self.clear_selection, bg="#e74c3c", fg="white", font=("Arial", 9)).pack(side=tk.LEFT, padx=3) self.selection_info = tk.Label(table_group, text="点击两个单元格选择范围", bg="#34495e", fg="#95a5a6", font=("Arial", 8), anchor=tk.W) self.selection_info.pack(fill=tk.X, pady=2) # 选择状态 - 点击两个点确定范围 self.select_start = None # 第一个点击 (r, c) self.select_end = None # 第二个点击 (r, c) self.selected_cells = [] # 当前选中的单元格列表 self.selection_color = "#3498db" # 选中颜色 # 切片大小和预览线设置 size_frame = tk.Frame(right_frame, bg="#34495e") size_frame.pack(fill=tk.X, padx=10, pady=5) tk.Label(size_frame, text="切片大小:", bg="#34495e", fg="#ecf0f1", font=("Arial", 9)).grid(row=0, column=0, sticky=tk.W) self.slice_size_entry = tk.Entry(size_frame, width=8, font=("Arial", 9)) self.slice_size_entry.grid(row=0, column=1, padx=5) self.slice_size_entry.insert(0, "256") self.slice_size_entry.bind('', lambda e: self.slice_size.set(int(self.slice_size_entry.get()))) self.slice_size_entry.bind('', lambda e: self.slice_size.set(int(self.slice_size_entry.get()))) # 预览线控制 self.show_preview_line = tk.BooleanVar(value=True) tk.Checkbutton(size_frame, text="显示预览线", variable=self.show_preview_line, bg="#34495e", fg="#ecf0f1", font=("Arial", 9), command=self.update_preview_lines).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2) # XLSX xlsx_group = tk.LabelFrame(right_frame, text="XLSX to CSV", font=("Arial", 11), bg="#34495e", fg="#27ae60", padx=10, pady=10) xlsx_group.pack(fill=tk.X, padx=10, pady=5) tk.Button(xlsx_group, text="Select XLSX", command=self.select_xlsx_file, bg="#27ae60", fg="white", font=("Arial", 9)).pack(fill=tk.X, pady=2) tk.Button(xlsx_group, text="Batch Convert All", command=self.batch_convert_xlsx, bg="#8e44ad", fg="white", font=("Arial", 9)).pack(fill=tk.X, pady=2) self.xlsx_info_label = tk.Label(xlsx_group, text="", bg="#34495e", fg="#95a5a6", font=("Arial", 8), anchor=tk.W) self.xlsx_info_label.pack(fill=tk.X, pady=5) def add_range(self): """添加新的d值范围""" # 获取最后一个范围的end值 if self.range_end_entries: last_end = int(self.range_end_entries[-1].get()) new_start = last_end # 左闭右开,下一个从last_end开始 new_end = min(last_end + 10, 101) # 默认加10,最多到101 else: new_start = 1 new_end = 101 # 左闭右开 [1, 101) 包含 1-100 self.d_ranges.append({'start': new_start, 'end': new_end, 'd': 1.0}) # 更新显示 self.update_range_display() def remove_range(self): """删除最后一个d值范围""" if len(self.d_ranges) > 1: self.d_ranges.pop() frame = self.range_frames.pop() frame.destroy() self.range_start_entries.pop() self.range_end_entries.pop() self.range_d_entries.pop() def calculate_all_depths(self): """根据d值范围计算所有深度""" # 更新d_ranges数据 for i in range(len(self.d_ranges)): try: self.d_ranges[i]['start'] = int(self.range_start_entries[i].get()) self.d_ranges[i]['end'] = int(self.range_end_entries[i].get()) self.d_ranges[i]['d'] = float(self.range_d_entries[i].get()) except: pass # 获取起始深度 try: base_depth = float(self.base_depth_entry.get()) except: base_depth = 3660 # 计算100个格子的深度 # 格子编号: 第r行第c列 = r*10 + c + 1 depths = [0] * 100 current_depth = base_depth for i in range(100): cell_num = i + 1 # 1-100 # 找到对应的d值 (左闭右开区间 [start, end)) d = 1.0 for r in self.d_ranges: if r['start'] <= cell_num < r['end']: d = r['d'] break depths[i] = current_depth current_depth += d # 更新depth_data for r in range(10): for c in range(10): idx = r * 10 + c self.depth_data[r][c] = depths[idx] self.update_depth_display() def update_depth_display(self): """更新深度显示""" for r in range(10): for c in range(10): val = self.depth_data[r][c] self.cell_labels[r][c].config(text=f"{val:.1f}" if val != int(val) else str(int(val))) def on_cell_edit(self, row, col): try: value = float(self.cell_labels[row][col].cget('text')) self.depth_data[row][col] = value except: pass def on_cell_select_start(self, row, col): """点击选择单元格 - 第一个点(按序号范围)""" cell_num = row * 10 + col + 1 if self.select_start is None: # 第一次点击 self.select_start = (row, col, cell_num) self.select_end = (row, col, cell_num) self.cell_labels[row][col].config(bg=self.selection_color) self.selection_info.config(text=f"已选: #{cell_num}, 点击第二个单元格") elif self.select_start[2] == cell_num: # 点击第一个单元格,取消选择 self.clear_selection() else: # 第二次点击 - 完成选择 self.select_end = (row, col, cell_num) self.update_selection() self.selection_info.config(text=f"序号范围: {len(self.selected_cells)} 个单元格,可设置d值") def merge_ranges(self, ranges): """合并相邻的相同d值范围""" if not ranges: return [] # 按起始点排序 sorted_ranges = sorted(ranges, key=lambda x: x['start']) merged = [sorted_ranges[0]] for r in sorted_ranges[1:]: last = merged[-1] if r['start'] <= last['end'] and r['d'] == last['d']: # 相邻且d值相同,合并 last['end'] = max(last['end'], r['end']) else: merged.append(r) return merged def update_range_display(self): """更新右侧范围输入框显示""" # 保存父容器引用 parent = self.range_frames[0].master if self.range_frames else None # 销毁现有输入框 for frame in self.range_frames: frame.destroy() self.range_frames = [] self.range_start_entries = [] self.range_end_entries = [] self.range_d_entries = [] if parent is None: return # 重新创建输入框 for r in self.d_ranges: frame = tk.Frame(parent, bg="#34495e") frame.pack(fill=tk.X, pady=2) start_entry = tk.Entry(frame, width=6, font=("Arial", 9), justify=tk.CENTER) start_entry.pack(side=tk.LEFT, padx=2) start_entry.insert(0, str(r['start'])) tk.Label(frame, text="~", bg="#34495e", fg="white").pack(side=tk.LEFT) end_entry = tk.Entry(frame, width=6, font=("Arial", 9), justify=tk.CENTER) end_entry.pack(side=tk.LEFT, padx=2) end_entry.insert(0, str(r['end'] if r['end'] <= 100 else 101)) tk.Label(frame, text="d=", bg="#34495e", fg="white").pack(side=tk.LEFT, padx=(10,2)) d_entry = tk.Entry(frame, width=8, font=("Arial", 9), justify=tk.CENTER) d_entry.pack(side=tk.LEFT, padx=2) d_entry.insert(0, str(r['d'])) self.range_frames.append(frame) self.range_start_entries.append(start_entry) self.range_end_entries.append(end_entry) self.range_d_entries.append(d_entry) def on_cell_select_drag(self, row, col): """拖动选择 - 不再使用""" pass def update_selection(self): """更新选中区域 - 按单元格序号顺序(支持跨行选区)""" # 清除之前的选中状态 for r in range(10): for c in range(10): self.cell_labels[r][c].config(bg="#1a1a2e") if self.select_start and self.select_end: num1 = self.select_start[2] # 起始序号 num2 = self.select_end[2] # 结束序号 min_num = min(num1, num2) max_num = max(num1, num2) self.selected_cells = [] for num in range(min_num, max_num + 1): row = (num - 1) // 10 col = (num - 1) % 10 self.cell_labels[row][col].config(bg=self.selection_color) self.selected_cells.append((row, col)) def on_cell_select_end(self): """选择结束""" pass # 保持选中状态,等待用户设置d值 def apply_selected_d(self): """应用d值到选中的单元格""" if not self.selected_cells: messagebox.showwarning("Warning", "请先选择单元格") return try: d_value = float(self.selected_d_entry.get()) except: messagebox.showwarning("Warning", "请输入有效的d值") return if len(self.selected_cells) == 1: r, c = self.selected_cells[0] cell_num = r * 10 + c + 1 self.depth_data[r][c] = d_value else: # 获取序号范围 cell_nums = [r * 10 + c + 1 for r, c in self.selected_cells] min_num = min(cell_nums) max_num = max(cell_nums) # 更新 d_ranges - 替换或更新覆盖该范围的范围 new_ranges = [] for rng in self.d_ranges: # 完全在选中范围外的保留 if rng['end'] <= min_num or rng['start'] > max_num: new_ranges.append(rng) else: # 有交集 - 需要拆分 # 范围左侧在选中范围外 if rng['start'] < min_num: new_ranges.append({'start': rng['start'], 'end': min_num, 'd': rng['d']}) # 范围右侧在选中范围外 if rng['end'] > max_num + 1: new_ranges.append({'start': max_num + 1, 'end': rng['end'], 'd': rng['d']}) # 添加新的选中范围 new_ranges.append({'start': min_num, 'end': max_num + 1, 'd': d_value}) # 合并相邻的相同d值范围 self.d_ranges = self.merge_ranges(new_ranges) # 重新计算所有深度 self.calculate_all_depths() # 更新右侧范围显示 self.update_range_display() messagebox.showinfo("Success", f"已设置 #{min(cell_nums)}-{max(cell_nums)} 的d值为 {d_value}") def apply_all_d(self): """设置所有100个单元格的d值""" try: d_value = float(self.selected_d_entry.get()) except: messagebox.showwarning("Warning", "请输入有效的d值") return # 选择所有单元格 self.selected_cells = [] for r in range(10): for c in range(10): self.selected_cells.append((r, c)) self.cell_labels[r][c].config(bg=self.selection_color) # 更新 d_ranges self.d_ranges = [{'start': 1, 'end': 101, 'd': d_value}] # 重新计算所有深度 self.calculate_all_depths() messagebox.showinfo("Success", f"已设置所有单元格的d值为 {d_value}") def get_d_for_cell(self, row, col): """获取指定单元格的d值""" cell_num = row * 10 + col + 1 for r in self.d_ranges: if r['start'] <= cell_num < r['end']: return r['d'] return 1.0 def clear_selection(self): """清除选择""" self.select_start = None self.select_end = None self.selected_cells = [] for r in range(10): for c in range(10): self.cell_labels[r][c].config(bg="#1a1a2e") self.selection_info.config(text="点击两个单元格选择范围") def update_preview_lines(self): """更新画布上的预览线""" if not self.show_preview_line.get() or len(self.corner_points) != 4: return # 删除旧的预览线 for line_id in getattr(self, 'preview_line_ids', []): self.canvas.delete(line_id) self.preview_line_ids = [] # 绘制新的预览线 grid_pts = self.get_grid_points(self.corner_points) # 水平线 for i in range(11): for j in range(10): p1 = grid_pts[i * 11 + j] p2 = grid_pts[i * 11 + j + 1] sx1 = int(p1[0] * self.scale) + self.offset_x sy1 = int(p1[1] * self.scale) + self.offset_y sx2 = int(p2[0] * self.scale) + self.offset_x sy2 = int(p2[1] * self.scale) + self.offset_y line_id = self.canvas.create_line(sx1, sy1, sx2, sy2, fill='#3498db', width=1) self.preview_line_ids.append(line_id) # 垂直线 for i in range(11): for j in range(10): p1 = grid_pts[j * 11 + i] p2 = grid_pts[(j + 1) * 11 + i] sx1 = int(p1[0] * self.scale) + self.offset_x sy1 = int(p1[1] * self.scale) + self.offset_y sx2 = int(p2[0] * self.scale) + self.offset_x sy2 = int(p2[1] * self.scale) + self.offset_y line_id = self.canvas.create_line(sx1, sy1, sx2, sy2, fill='#27ae60', width=1) self.preview_line_ids.append(line_id) def clear_points(self): self.corner_points = [] for mid in self.temp_marker_ids: self.canvas.delete(mid) self.temp_marker_ids = [] # 删除预览线 for line_id in getattr(self, 'preview_line_ids', []): self.canvas.delete(line_id) self.preview_line_ids = [] self.update_progress() self.draw_image_and_grid() def clear_selection(self): """清除选择""" self.select_start = None self.select_end = None self.selected_cells = [] for r in range(10): for c in range(10): self.cell_labels[r][c].config(bg="#1a1a2e") self.selection_info.config(text="点击两个单元格选择范围") def update_progress(self): cnt = len(self.corner_points) self.progress_label.config(text=f"Corner Points: {cnt}/4") def on_canvas_click(self, event): if self.current_image is None: return x, y = event.x, event.y if x < self.offset_x or x > self.offset_x + self.display_w: return if y < self.offset_y or y > self.offset_y + self.display_h: return ix = int((x - self.offset_x) / self.scale) iy = int((y - self.offset_y) / self.scale) # 检查是否点击了现有的角点(用于拖动) hit_point = None for i, (px, py) in enumerate(self.corner_points): sx = int(px * self.scale) + self.offset_x sy = int(py * self.scale) + self.offset_y if abs(x - sx) < 20 and abs(y - sy) < 20: hit_point = i break if hit_point is not None: # 拖动现有角点 self.dragging_point = hit_point self.canvas.bind('', self.on_canvas_drag) self.canvas.bind('', self.on_canvas_drag_end) else: # 添加新点 if len(self.corner_points) < 4: self.corner_points.append((ix, iy)) # 标记点 - 按顺序: 1-左上, 2-右上, 3-左下, 4-右下 labels = ["1-左上", "2-右上", "3-左下", "4-右下"] pid = self.canvas.create_oval(x-12, y-12, x+12, y+12, fill="#f1c40f", outline="#e67e22", width=3) self.temp_marker_ids.append(pid) pid = self.canvas.create_text(x, y-22, text=labels[len(self.corner_points)-1], fill="#f1c40f", font=("Arial", 11, "bold")) self.temp_marker_ids.append(pid) self.update_progress() # 已有4个点时,绘制预览线 if len(self.corner_points) == 4: self.update_preview_lines() def on_canvas_drag(self, event): """拖动角点""" if not hasattr(self, 'dragging_point') or self.dragging_point is None: return x, y = event.x, event.y # 检查边界 if x < self.offset_x or x > self.offset_x + self.display_w: return if y < self.offset_y or y > self.offset_y + self.display_h: return ix = int((x - self.offset_x) / self.scale) iy = int((y - self.offset_y) / self.scale) # 更新点位置 self.corner_points[self.dragging_point] = (ix, iy) # 重新绘制角点标记 self.draw_corner_markers() # 更新预览线 self.update_preview_lines() def draw_image_only(self): """仅绘制图像,不清除标记""" if self.canvas is None or self.current_image is None: return canvas_w = self.canvas.winfo_width() canvas_h = self.canvas.winfo_height() if canvas_w < 10 or canvas_h < 10: canvas_w, canvas_h = 1000, 850 img_w, img_h = self.current_image.size scale = min(canvas_w / img_w, canvas_h / img_h) new_w = int(img_w * scale) new_h = int(img_h * scale) offset_x = (canvas_w - new_w) // 2 offset_y = (canvas_h - new_h) // 2 self.scale = scale self.offset_x = offset_x self.offset_y = offset_y self.display_w = new_w self.display_h = new_h # 清除预览线和角点 self.canvas.delete("all") self.temp_marker_ids = [] display_img = self.current_image.copy().resize((new_w, new_h), Image.LANCZOS) photo = ImageTk.PhotoImage(display_img) self.canvas.create_image(offset_x, offset_y, anchor=tk.NW, image=photo) self.canvas.image = photo # 重绘角点标记 self.draw_corner_markers() def draw_corner_markers(self): """绘制角点标记""" # 清除旧的角点标记 for mid in self.temp_marker_ids: self.canvas.delete(mid) self.temp_marker_ids = [] labels = ["1-左上", "2-右上", "3-左下", "4-右下"] for i, (x, y) in enumerate(self.corner_points): sx = int(x * self.scale) + self.offset_x sy = int(y * self.scale) + self.offset_y pid = self.canvas.create_oval(sx-12, sy-12, sx+12, sy+12, fill="#f1c40f", outline="#e67e22", width=3) self.temp_marker_ids.append(pid) pid = self.canvas.create_text(sx, sy-22, text=labels[i], fill="#f1c40f", font=("Arial", 11, "bold")) self.temp_marker_ids.append(pid) def on_canvas_drag_end(self, event): """拖动结束""" self.dragging_point = None self.canvas.unbind('') self.canvas.unbind('') def validate_quadrilateral(self): """验证四个点能否形成平行四边形""" pts = self.corner_points if len(pts) != 4: return False # 计算向量 p0, p1, p2, p3 = pts # 向量 v01 = (p1[0] - p0[0], p1[1] - p0[1]) # 边1 v02 = (p2[0] - p0[0], p2[1] - p0[1]) # 边2 v13 = (p3[0] - p1[0], p3[1] - p1[1]) # 边3 v23 = (p3[0] - p2[0], p3[1] - p2[1]) # 边4 def is_parallel(v1, v2, tolerance=5): """判断两向量是否平行 (叉积为0)""" cross = abs(v1[0] * v2[1] - v1[1] * v2[0]) len1 = (v1[0]**2 + v1[1]**2) ** 0.5 len2 = (v2[0]**2 + v2[1]**2) ** 0.5 if len1 < 1 or len2 < 1: return False return cross / (len1 * len2) < tolerance / 100 # 容许5%误差 # 检查两组对边平行 if is_parallel(v01, v23) and is_parallel(v02, v13): return True # 尝试交换对边组合 if is_parallel(v01, v13) and is_parallel(v02, v23): return True return False def sort_points_to_quadrilateral(self): """将4个点排序为 TL, TR, BR, BL(左上、右上、右下、左下)""" pts = self.corner_points # 1. 按 x 坐标分组:左边两个和右边两个 sorted_by_x = sorted(pts, key=lambda p: p[0]) left_pts = sorted_by_x[:2] right_pts = sorted_by_x[2:] # 2. 每组内按 y 排序 left_pts = sorted(left_pts, key=lambda p: p[1]) # 上在下之前 right_pts = sorted(right_pts, key=lambda p: p[1]) # 上在下之前 # 3. 组合为 TL, TR, BR, BL self.corner_points = [left_pts[0], right_pts[0], right_pts[1], left_pts[1]] def draw_image_and_grid(self): if self.canvas is None: return self.canvas.delete("all") if self.current_image is None: self.canvas.create_text(500, 425, text="Load an image to start", fill="#95a5a6", font=("Arial", 16)) return canvas_w = self.canvas.winfo_width() canvas_h = self.canvas.winfo_height() if canvas_w < 10 or canvas_h < 10: canvas_w, canvas_h = 1000, 850 img_w, img_h = self.current_image.size scale = min(canvas_w / img_w, canvas_h / img_h) new_w = int(img_w * scale) new_h = int(img_h * scale) offset_x = (canvas_w - new_w) // 2 offset_y = (canvas_h - new_h) // 2 self.scale = scale self.offset_x = offset_x self.offset_y = offset_y self.display_w = new_w self.display_h = new_h display_img = self.current_image.copy().resize((new_w, new_h), Image.LANCZOS) photo = ImageTk.PhotoImage(display_img) self.canvas.create_image(offset_x, offset_y, anchor=tk.NW, image=photo) self.canvas.image = photo # 保持引用 self.canvas.create_rectangle(offset_x, offset_y, offset_x + new_w, offset_y + new_h, outline="#3498db", width=4) # 重绘角点标记 labels = ["1-左上", "2-右上", "3-左下", "4-右下"] for i, (x, y) in enumerate(self.corner_points): sx = int(x * scale) + offset_x sy = int(y * scale) + offset_y pid = self.canvas.create_oval(sx-12, sy-12, sx+12, sy+12, fill="#f1c40f", outline="#e67e22", width=3) self.temp_marker_ids.append(pid) pid = self.canvas.create_text(sx, sy-22, text=labels[i], fill="#f1c40f", font=("Arial", 11, "bold")) self.temp_marker_ids.append(pid) self.update_progress() def select_image(self): file_path = filedialog.askopenfilename( title="Select Image", filetypes=[("Image files", "*.avif *.jpg *.jpeg *.png *.webp"), ("All files", "*.*")] ) if file_path: self.load_image(file_path) def load_image(self, file_path): try: self.current_image_path = file_path self.current_image = Image.open(file_path) self.corner_points = [] self.temp_marker_ids = [] filename = os.path.basename(file_path) self.image_info_label.config(text=f"Loaded: {filename} | {self.current_image.size[0]}x{self.current_image.size[1]}") # 从文件名解析起始深度 basename = os.path.splitext(filename)[0] if '-' in basename: parts = basename.split('-') if len(parts) == 2: try: start_depth = int(parts[0]) self.base_depth_entry.delete(0, tk.END) self.base_depth_entry.insert(0, str(start_depth)) except: pass self.root.after(100, self.draw_image_and_grid) messagebox.showinfo("Success", f"Image loaded!\n\nClick 4 corners to set region") except Exception as e: messagebox.showerror("Error", f"Cannot load image: {str(e)}") def select_output_dir(self): dir_path = filedialog.askdirectory(title="Select Output Directory") if dir_path: self.output_dir.set(dir_path) def get_grid_points(self, src_pts): """计算4点形成的四边形内的10x10等分点""" grid_pts = [] # 四个角点: 0-左上(TL), 1-右上(TR), 2-左下(BL), 3-右下(BR) tl, tr, bl, br = src_pts[0], src_pts[1], src_pts[2], src_pts[3] for i in range(11): for j in range(11): u = i / 10.0 v = j / 10.0 # 上下边的线性插值 top_x = (1-v) * tl[0] + v * tr[0] top_y = (1-v) * tl[1] + v * tr[1] bottom_x = (1-v) * bl[0] + v * br[0] bottom_y = (1-v) * bl[1] + v * br[1] # 最终点的插值 x = (1-u) * top_x + u * bottom_x y = (1-u) * top_y + u * bottom_y grid_pts.append((x, y)) return grid_pts def execute_slice(self): if self.current_image is None: messagebox.showwarning("Warning", "Please load an image first") return if len(self.corner_points) != 4: messagebox.showwarning("Warning", f"Please click 4 corner points ({len(self.corner_points)}/4)") return try: output_dir = self.output_dir.get() if not os.path.exists(output_dir): os.makedirs(output_dir) img_name = os.path.splitext(os.path.basename(self.current_image_path))[0] slice_dir = os.path.join(output_dir, f"{img_name}_slices") os.makedirs(slice_dir, exist_ok=True) img_w, img_h = self.current_image.size slice_size = self.slice_size.get() grid_pts = self.get_grid_points(self.corner_points) count = 0 for r in range(10): for c in range(10): tl = r * 11 + c tr = r * 11 + c + 1 br = (r + 1) * 11 + c + 1 bl = (r + 1) * 11 + c pts = [grid_pts[tl], grid_pts[tr], grid_pts[br], grid_pts[bl]] # 检查坐标有效性 if not all(p and len(p) == 2 for p in pts): continue slice_img = self.extract_quad(self.current_image, pts) slice_img = slice_img.resize((slice_size, slice_size), Image.LANCZOS) depth = self.depth_data[r][c] end_depth = depth + 1.0 # 使用行列号确保文件名唯一,避免深度相同时覆盖 slice_filename = f"slice_{r+1:02d}_{c+1:02d}_depth_{int(depth)}_{int(end_depth)}.png" slice_img.save(os.path.join(slice_dir, slice_filename)) count += 1 # 保存带网格预览 grid_img = self.current_image.copy().convert("RGB") draw = ImageDraw.Draw(grid_img) for i, (x, y) in enumerate(self.corner_points): draw.ellipse([x-5, y-5, x+5, y+5], fill='red') for i in range(11): for j in range(10): p1 = grid_pts[i * 11 + j] p2 = grid_pts[i * 11 + j + 1] draw.line([int(p1[0]), int(p1[1]), int(p2[0]), int(p2[1])], fill='blue', width=2) for j in range(10): p1 = grid_pts[j * 11 + i] p2 = grid_pts[(j + 1) * 11 + i] draw.line([int(p1[0]), int(p1[1]), int(p2[0]), int(p2[1])], fill='green', width=2) grid_img.save(os.path.join(slice_dir, f"{img_name}_grid.png")) messagebox.showinfo("Success", f"Sliced {count} images!\nOutput: {slice_dir}") except Exception as e: messagebox.showerror("Error", f"Slice failed: {str(e)}") def extract_quad(self, img, pts): # 使用 round() 四舍五入代替 int() 截断,避免多边形缩小 int_pts = [tuple(round(p[i]) for i in range(2)) for p in pts] min_x = max(0, min(p[0] for p in int_pts)) max_x = min(img.width, max(p[0] for p in int_pts) + 1) min_y = max(0, min(p[1] for p in int_pts)) max_y = min(img.height, max(p[1] for p in int_pts) + 1) width = max(1, max_x - min_x) height = max(1, max_y - min_y) crop = img.crop((min_x, min_y, max_x, max_y)) result = Image.new('RGBA', (width, height), (0, 0, 0, 0)) mask = Image.new('L', (width, height), 0) mask_draw = ImageDraw.Draw(mask) local_pts = [(p[0] - min_x, p[1] - min_y) for p in int_pts] mask_draw.polygon(local_pts, fill=255) result.paste(crop, (0, 0)) result.putalpha(mask) result_rgb = Image.new('RGB', (width, height), (0, 0, 0)) result_rgb.paste(result, (0, 0), mask) return result_rgb def select_xlsx_file(self): file_path = filedialog.askopenfilename(title="Select XLSX", filetypes=[("Excel", "*.xlsx *.xls"), ("All", "*.*")]) if file_path: self.convert_xlsx_to_csv(file_path) def convert_xlsx_to_csv(self, xlsx_path): try: import time df = pd.read_excel(xlsx_path) csv_path = xlsx_path.replace('.xlsx', '.csv').replace('.xls', '.csv') if os.path.exists(csv_path): csv_path = f"{os.path.splitext(csv_path)[0]}_{int(time.time())}.csv" df.to_csv(csv_path, index=False, encoding='utf-8-sig') self.xlsx_info_label.config(text=f"Converted: {os.path.basename(csv_path)}") messagebox.showinfo("Success", "CSV saved!") except Exception as e: messagebox.showerror("Error", f"Conversion failed: {str(e)}") def batch_convert_xlsx(self): root_dir = "岩屑图像" converted = [] if not os.path.exists(root_dir): messagebox.showwarning("Warning", "Directory not found") return for folder in os.listdir(root_dir): folder_path = os.path.join(root_dir, folder) if os.path.isdir(folder_path): for filename in os.listdir(folder_path): if filename.endswith(('.xlsx', '.xls')): try: df = pd.read_excel(os.path.join(folder_path, filename)) csv_path = os.path.join(folder_path, filename.replace('.xlsx', '.csv').replace('.xls', '.csv')) df.to_csv(csv_path, index=False, encoding='utf-8-sig') converted.append(csv_path) except: pass if converted: messagebox.showinfo("Done", f"Converted {len(converted)} files") else: messagebox.showwarning("Warning", "No files found") if __name__ == "__main__": root = tk.Tk() app = SliceToolApp(root) root.mainloop()