AI生成的程序。
功能:
1、划定曲线。根据划定曲线,产生正弦的随机波动的mask。可调节振幅、频率、线宽。
2、通过鼠标点击划定特殊形状区域。保存为mask图像
程序界面图如下:
生成随机曲线。

划定特殊区域mask

代码如下:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
from scipy.interpolate import splprep, splev
import math
import random
from PIL import Image, ImageTk, ImageDraw
import os
class WaveEditorApp:
def __init__(self, master):
self.master = master
self.master.title("高级波浪与形状生成器 (Tkinter版)")
self.master.state('zoomed') # 窗口最大化启动
# --- 核心状态变量 ---
self.original_image = None
self.tk_image = None
self.control_points = []
self.closed_shapes = [] # 存储所有已闭合的多边形
self.wavy_curve = None
self.base_curve = None
self.width_control_nodes = None
self.vertical_offset = 0
self.selected_width_node_idx = -1
self.current_mode = None # 'WAVE' or 'SHAPE'
# --- UI 状态变量 ---
self.image_path_var = tk.StringVar(value="未加载图片")
self.output_filename_var = tk.StringVar(value="my_artwork")
self.setup_ui()
def setup_ui(self):
# --- 主框架 ---
main_frame = ttk.Frame(self.master)
main_frame.pack(fill=tk.BOTH, expand=True)
# --- 控制面板 (下方) ---
control_frame = ttk.Frame(main_frame, padding=10)
control_frame.pack(side=tk.BOTTOM, fill=tk.X)
# --- 画布 (上方) ---
self.canvas = tk.Canvas(main_frame, bg="#333333", highlightthickness=0)
self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.canvas.bind("<Button-1>", self.on_canvas_click)
# --- 组织下方控制面板的UI ---
self.build_control_panel(control_frame)
def build_control_panel(self, parent_frame):
# 创建一个大的框架来容纳所有控制组
top_controls = ttk.Frame(parent_frame)
top_controls.pack(fill=tk.X)
bottom_info = ttk.Frame(parent_frame)
bottom_info.pack(fill=tk.X, pady=(10,0))
# -- 第一行控制按钮 --
# 文件操作
file_box = ttk.LabelFrame(top_controls, text="文件")
file_box.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5, anchor='n')
ttk.Button(file_box, text="加载图片", command=self.load_image).pack(pady=5, padx=5)
ttk.Button(file_box, text="保存效果图", command=lambda: self.save_image(is_mask=False)).pack(pady=5, padx=5)
# 曲线与形状操作
action_box = ttk.LabelFrame(top_controls, text="操作")
action_box.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5, anchor='n')
ttk.Button(action_box, text="生成波浪曲线 (D)", command=self.generate_wave).pack(pady=5, padx=5)
ttk.Button(action_box, text="闭合当前形状 (C)", command=self.close_current_shape).pack(pady=5, padx=5)
ttk.Button(action_box, text="保存形状Mask", command=lambda: self.save_image(is_mask=True, shape_mask=True)).pack(pady=5, padx=5)
ttk.Button(action_box, text="保存波浪Mask", command=lambda: self.save_image(is_mask=True, shape_mask=False)).pack(pady=5, padx=5)
ttk.Button(action_box, text="重置/清除 (R)", command=self.reset_all).pack(pady=5, padx=5, side=tk.BOTTOM)
self.master.bind('<d>', lambda event: self.generate_wave())
self.master.bind('<r>', lambda event: self.reset_all())
self.master.bind('<c>', lambda event: self.close_current_shape())
# 参数范围控制
self.sliders = {}
param_box = ttk.LabelFrame(top_controls, text="波浪曲线参数随机范围")
param_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
self.sliders['振幅'] = self.create_range_slider(param_box, "振幅", 5, 80, (10, 40))
self.sliders['频率'] = self.create_range_slider(param_box, "频率", 10, 150, (20, 80))
self.sliders['宽度'] = self.create_range_slider(param_box, "宽度", 1, 30, (3, 12))
# 后期调整
adjust_box = ttk.LabelFrame(top_controls, text="波浪曲线后期调整")
adjust_box.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5, anchor='n')
shift_frame = ttk.Frame(adjust_box)
shift_frame.pack(fill=tk.X, pady=5, padx=5)
ttk.Button(shift_frame, text="上移 (W)", command=lambda: self.shift_curve(-10)).pack()
ttk.Button(shift_frame, text="下移 (S)", command=lambda: self.shift_curve(10)).pack(pady=5)
self.master.bind('<w>', lambda event: self.shift_curve(-10))
self.master.bind('<s>', lambda event: self.shift_curve(10))
self.width_label = ttk.Label(adjust_box, text="局部宽度: (先点击曲线)")
self.width_label.pack(padx=5, pady=(10, 0))
self.width_slider = ttk.Scale(adjust_box, from_=1, to=100, orient=tk.HORIZONTAL, command=self.adjust_local_width, state=tk.DISABLED)
self.width_slider.pack(fill=tk.X, padx=5, pady=5)
# -- 第二行信息与文件名 --
ttk.Label(bottom_info, text="当前图片路径:").pack(side=tk.LEFT, padx=(0,5))
ttk.Label(bottom_info, textvariable=self.image_path_var, foreground="blue").pack(side=tk.LEFT, expand=True, fill=tk.X)
ttk.Label(bottom_info, text="拟保存文件名:").pack(side=tk.LEFT, padx=(10,5))
ttk.Entry(bottom_info, textvariable=self.output_filename_var, width=30).pack(side=tk.LEFT)
ttk.Label(bottom_info, text=".png").pack(side=tk.LEFT)
def create_range_slider(self, parent, name, min_val, max_val, defaults):
frame = ttk.Frame(parent)
frame.pack(fill=tk.X, padx=10, pady=5, expand=True)
label = ttk.Label(frame, text=f"{name}范围: {defaults[0]}-{defaults[1]}")
label.pack(side=tk.LEFT, fill=tk.X)
min_var = tk.IntVar(value=defaults[0])
max_var = tk.IntVar(value=defaults[1])
def update_label(*args):
min_v, max_v = min_var.get(), max_var.get()
if min_v > max_v: min_var.set(max_v); min_v = max_v
label.config(text=f"{name}范围: {min_v}-{max_v}")
max_slider = ttk.Scale(frame, from_=min_val, to=max_val, variable=max_var, orient=tk.HORIZONTAL, command=update_label)
max_slider.pack(side=tk.RIGHT, fill=tk.X, expand=True)
min_slider = ttk.Scale(frame, from_=min_val, to=max_val, variable=min_var, orient=tk.HORIZONTAL, command=update_label)
min_slider.pack(side=tk.RIGHT, fill=tk.X, expand=True)
return {'min': min_var, 'max': max_var}
# --- 功能函数 ---
def load_image(self):
path = filedialog.askopenfilename(title="请选择一张图片", filetypes=[("Image Files", "*.jpg *.jpeg *.png *.bmp")])
if not path: return
try:
self.original_image = Image.open(path).convert("RGB")
self.image_path_var.set(os.path.basename(path))
self.reset_all()
w, h = self.original_image.size
self.canvas.config(width=w, height=h)
self.tk_image = ImageTk.PhotoImage(self.original_image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_image)
except Exception as e:
messagebox.showerror("加载失败", f"无法加载图片: {e}")
def reset_all(self):
self.control_points.clear()
self.closed_shapes.clear()
self.wavy_curve = None
self.base_curve = None
self.vertical_offset = 0
self.selected_width_node_idx = -1
self.width_slider.config(state=tk.DISABLED)
self.width_label.config(text="局部宽度: (先点击曲线)")
self.current_mode = None
self.update_canvas()
def on_canvas_click(self, event):
if not self.original_image:
messagebox.showwarning("提示", "请先加载一张图片!")
return
if self.current_mode == 'WAVE':
self.select_width_node((event.x, event.y))
else: # 初始状态或形状模式
self.control_points.append((event.x, event.y))
self.update_canvas()
def generate_wave(self):
if not self.control_points:
messagebox.showwarning("提示", "请先点击鼠标定义路径!")
return
if len(self.control_points) < 2:
messagebox.showwarning("提示", "请至少点击2个点以生成曲线。")
return
# 进入波浪模式,清除其他模式的产物
self.closed_shapes.clear()
self.current_mode = 'WAVE'
self.vertical_offset = 0
print("正在生成波浪曲线...")
self.base_curve, self.wavy_curve, self.width_control_nodes = self.create_wavy_curve(self.control_points)
self.update_canvas()
print("生成完毕。现在可以点击曲线调整局部宽度,或使用W/S键平移。")
def close_current_shape(self):
if len(self.control_points) < 3:
messagebox.showwarning("提示", "请至少点击3个点以构成一个形状。")
return
# 进入形状模式,清除其他模式的产物
if self.current_mode != 'SHAPE':
self.wavy_curve = None
self.base_curve = None
self.vertical_offset = 0
self.current_mode = 'SHAPE'
self.closed_shapes.append(list(self.control_points))
self.control_points.clear()
self.update_canvas()
print(f"已闭合一个形状。当前共 {len(self.closed_shapes)} 个形状。可继续绘制下一个。")
def update_canvas(self):
self.canvas.delete("overlay")
if not self.original_image: return
# 绘制所有已闭合的形状
if self.closed_shapes:
for shape_points in self.closed_shapes:
self.canvas.create_polygon(shape_points, fill="cyan", stipple="gray50", outline="cyan", width=2, tags="overlay")
# 绘制当前正在勾勒的控制点和引导线
if self.control_points:
if len(self.control_points) > 1:
self.canvas.create_line(self.control_points, fill="lime", width=2, tags="overlay", dash=(5, 3))
for x, y in self.control_points:
self.canvas.create_oval(x-4, y-4, x+4, y+4, fill="lime", outline="", tags="overlay")
# 绘制波浪曲线
if self.wavy_curve:
offset_curve = [(p[0], p[1] + self.vertical_offset, p[2]) for p in self.wavy_curve]
for i in range(len(offset_curve) - 1):
p1, p2 = offset_curve[i], offset_curve[i+1]
width = p1[2]
self.canvas.create_line(p1[0], p1[1], p2[0], p2[1], fill="#C81E32", width=width, tags="overlay", capstyle=tk.ROUND, joinstyle=tk.ROUND, smooth=True)
# 高亮选中的宽度节点
if self.selected_width_node_idx != -1 and self.base_curve is not None:
node_progress = self.width_control_nodes[0][self.selected_width_node_idx]
point_idx = int(node_progress * (len(self.base_curve) - 1))
x, y = self.base_curve[point_idx]
y += self.vertical_offset
self.canvas.create_oval(x-8, y-8, x+8, y+8, fill="yellow", outline="gold", width=2, tags="overlay")
def save_image(self, is_mask=False, shape_mask=False):
if not self.wavy_curve and not self.closed_shapes:
messagebox.showerror("错误", "没有任何可保存的内容!")
return
file_type_name = "形状Mask" if shape_mask else ("波浪Mask" if is_mask else "效果图")
initial_file = f"{self.output_filename_var.get()}_{'shape_mask' if shape_mask else ('wave_mask' if is_mask else 'result')}.png"
path = filedialog.asksaveasfilename(title=f"保存{file_type_name}", initialfile=initial_file, filetypes=[("PNG Image", "*.png")])
if not path: return
if is_mask:
img_to_save = Image.new("L", self.original_image.size, 0) # 8位黑白
draw = ImageDraw.Draw(img_to_save)
if shape_mask:
for shape_points in self.closed_shapes:
draw.polygon(shape_points, fill=255, outline=255)
else: # wave mask
if not self.wavy_curve: return
offset_curve = [(p[0], p[1] + self.vertical_offset, p[2]) for p in self.wavy_curve]
for i in range(len(offset_curve) - 1):
p1, p2 = offset_curve[i], offset_curve[i+1]
draw.line((p1[0], p1[1], p2[0], p2[1]), fill=255, width=int(p1[2]), joint="curve")
else: # 保存效果图
img_to_save = self.original_image.copy()
draw = ImageDraw.Draw(img_to_save)
if self.closed_shapes:
from PIL import Image, ImageDraw
overlay = Image.new('RGBA', img_to_save.size, (255,255,255,0))
overlay_draw = ImageDraw.Draw(overlay)
for shape_points in self.closed_shapes:
overlay_draw.polygon(shape_points, fill=(0, 255, 255, 128), outline=(0, 255, 255, 220))
img_to_save = Image.alpha_composite(img_to_save.convert('RGBA'), overlay).convert('RGB')
draw = ImageDraw.Draw(img_to_save)
if self.wavy_curve:
offset_curve = [(p[0], p[1] + self.vertical_offset, p[2]) for p in self.wavy_curve]
for i in range(len(offset_curve) - 1):
p1, p2 = offset_curve[i], offset_curve[i+1]
draw.line((p1[0], p1[1], p2[0], p2[1]), fill=(200, 30, 50), width=int(p1[2]), joint="curve")
img_to_save.save(path)
messagebox.showinfo("成功", f"{file_type_name}已保存至:\n{path}")
# --- 曲线计算与调整的辅助函数 ---
def create_wavy_curve(self, points, num_samples=800):
# (这部分核心算法与上一版基本相同)
points_np = np.array(points).T
k = min(3, points_np.shape[1] - 1)
if k<1: return None, None, None
tck, u = splprep([points_np[0], points_np[1]], s=0, k=k)
t_fine = np.linspace(0, 1, num_samples)
x_fine, y_fine = splev(t_fine, tck)
base_curve = np.vstack((x_fine, y_fine)).T
get_range = lambda name: (self.sliders[name]['min'].get(), self.sliders[name]['max'].get())
width_nodes = self.generate_random_nodes(10, *get_range("宽度"))
amp_func = self.generate_1d_spline_from_nodes(self.generate_random_nodes(7, *get_range("振幅")))
freq_func = self.generate_1d_spline_from_nodes(self.generate_random_nodes(9, *get_range("频率")))
width_func = self.generate_1d_spline_from_nodes(width_nodes)
wavy_points = []
current_phase = random.uniform(0, 2 * math.pi)
for i in range(num_samples - 1):
p1, p2 = base_curve[i], base_curve[i+1]
tangent = p2 - p1
norm = np.linalg.norm(tangent)
if norm == 0: continue
normal = np.array([-tangent[1], tangent[0]]) / norm
progress = t_fine[i]
amplitude, frequency, width = amp_func(progress), freq_func(progress), width_func(progress)
current_phase += frequency * (1.0 / num_samples)
displacement = amplitude * math.sin(current_phase)
new_point = p1 + normal * displacement
wavy_points.append((new_point[0], new_point[1], width))
return base_curve, wavy_points, width_nodes
def generate_random_nodes(self, num_nodes, min_val, max_val):
x = np.linspace(0, 1, num_nodes)
y = np.array([random.uniform(min_val, max_val) for _ in range(num_nodes)])
return x, y
def generate_1d_spline_from_nodes(self, nodes):
x, y = nodes
k = min(3, len(x) - 1)
if k < 1: return lambda t: y[0] if len(y) > 0 else 0
tck, _ = splprep([x, y], s=0, k=k)
return lambda t: splev(t, tck)[1]
def shift_curve(self, dy):
if self.current_mode != 'WAVE' or not self.wavy_curve: return
self.vertical_offset += dy
self.update_canvas()
def select_width_node(self, click_pos):
if not self.base_curve: return
click_pt_np = np.array([click_pos[0], click_pos[1] - self.vertical_offset])
distances = np.sqrt(np.sum((self.base_curve - click_pt_np)**2, axis=1))
closest_idx_on_curve = np.argmin(distances)
if distances[closest_idx_on_curve] > 50:
self.selected_width_node_idx = -1
self.width_slider.config(state=tk.DISABLED)
self.width_label.config(text="局部宽度: (先点击曲线)")
return
progress_t = closest_idx_on_curve / len(self.base_curve)
node_positions = self.width_control_nodes[0]
self.selected_width_node_idx = np.argmin(np.abs(node_positions - progress_t))
current_width = self.width_control_nodes[1][self.selected_width_node_idx]
self.width_slider.config(state=tk.NORMAL)
self.width_slider.set(current_width)
self.width_label.config(text=f"局部宽度 (节点 {self.selected_width_node_idx+1}):")
def adjust_local_width(self, value_str):
if self.selected_width_node_idx == -1: return
value = float(value_str)
self.width_control_nodes[1][self.selected_width_node_idx] = value
width_func = self.generate_1d_spline_from_nodes(self.width_control_nodes)
self.wavy_curve = [(p[0], p[1], width_func(i/len(self.wavy_curve))) for i, p in enumerate(self.wavy_curve)]
self.update_canvas()
if __name__ == "__main__":
root = tk.Tk()
app = WaveEditorApp(root)
root.mainloop()

964

被折叠的 条评论
为什么被折叠?



