visual_tool/silce_tool

1041 lines
42 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
岩屑图像切片工具 - 四点定位切分
按范围设置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("<Button-1>", 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('<Button-1>', lambda e, r=r, c=c: self.on_cell_select_start(r, c))
lbl.bind('<B1-Motion>', lambda e, r=r, c=c: self.on_cell_select_drag(r, c))
lbl.bind('<ButtonRelease-1>', 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('<FocusOut>', lambda e: self.slice_size.set(int(self.slice_size_entry.get())))
self.slice_size_entry.bind('<Return>', 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('<B1-Motion>', self.on_canvas_drag)
self.canvas.bind('<ButtonRelease-1>', 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('<B1-Motion>')
self.canvas.unbind('<ButtonRelease-1>')
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()