根据划定曲线,产生随机图案程序 --正弦曲线图案生成

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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值