import json import random import subprocess import threading from pathlib import Path import tkinter as tk from tkinter import ttk, filedialog, messagebox, colorchooser from PIL import Image, ImageTk # ========================================================= # CONFIG # ========================================================= PRESET_DIR = Path("presets") PRESET_DIR.mkdir(exist_ok=True) SUPPORTED_EXTENSIONS = ["*.jpg", "*.jpeg", "*.png", "*.webp"] # ========================================================= # APP # ========================================================= class RandomizerApp: def __init__(self, root): self.root = root self.root.title("Advanced Image Randomizer") self.root.geometry("1400x900") self.processing = False self.stop_requested = False self.custom_shape_color = None self.verify_imagemagick() self.create_variables() self.build_ui() # ===================================================== # VERIFY IMAGEMAGICK # ===================================================== def verify_imagemagick(self): try: subprocess.run( ["magick", "-version"], capture_output=True, check=True ) except Exception: messagebox.showerror( "ImageMagick Missing", "Install ImageMagick and add it to PATH." ) self.root.destroy() # ===================================================== # VARIABLES # ===================================================== def create_variables(self): self.single_image_var = tk.StringVar() self.repeat_count_var = tk.IntVar(value=100) self.input_var = tk.StringVar() self.output_var = tk.StringVar() self.mode_var = tk.StringVar(value="rgb") self.enable_shapes_var = tk.BooleanVar(value=True) self.enable_noise_var = tk.BooleanVar(value=True) self.enable_blur_var = tk.BooleanVar(value=True) self.enable_sharpen_var = tk.BooleanVar(value=True) self.enable_rotate_var = tk.BooleanVar(value=True) self.enable_resize_var = tk.BooleanVar(value=True) self.enable_roll_var = tk.BooleanVar(value=True) self.enable_rgb_shift_var = tk.BooleanVar(value=True) self.enable_preview_var = tk.BooleanVar(value=True) self.strength_var = tk.DoubleVar(value=1.0) self.shape_count_var = tk.IntVar(value=3) self.noise_min_var = tk.DoubleVar(value=0.5) self.noise_max_var = tk.DoubleVar(value=3.0) self.rotate_min_var = tk.DoubleVar(value=-1.0) self.rotate_max_var = tk.DoubleVar(value=1.0) self.blur_max_var = tk.DoubleVar(value=0.7) self.sharpen_max_var = tk.DoubleVar(value=2.0) self.quality_min_var = tk.IntVar(value=75) self.quality_max_var = tk.IntVar(value=99) self.roll_max_var = tk.IntVar(value=8) self.resize_min_var = tk.DoubleVar(value=97.5) self.resize_max_var = tk.DoubleVar(value=102.5) self.shape_size_min_var = tk.IntVar(value=20) self.shape_size_max_var = tk.IntVar(value=80) self.shape_alpha_min_var = tk.DoubleVar(value=0.2) self.shape_alpha_max_var = tk.DoubleVar(value=0.7) # ===================================================== # UI # ===================================================== def build_ui(self): main = ttk.Frame(self.root, padding=10) main.pack(fill="both", expand=True) # LEFT left_container = ttk.Frame(main) left_container.pack(side="left", fill="y") canvas = tk.Canvas(left_container, width=420) scrollbar = ttk.Scrollbar( left_container, orient="vertical", command=canvas.yview ) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure( scrollregion=canvas.bbox("all") ) ) canvas.create_window( (0, 0), window=scrollable_frame, anchor="nw" ) canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="y") scrollbar.pack(side="right", fill="y") left = scrollable_frame # RIGHT right = ttk.Frame(main) right.pack(side="right", fill="both", expand=True) # ================================================= # FOLDERS # ================================================= path_frame = ttk.LabelFrame(left, text="Folders") path_frame.pack(fill="x", pady=5) ttk.Label(path_frame, text="Input Folder").pack(anchor="w") ttk.Entry( path_frame, textvariable=self.input_var ).pack(fill="x") ttk.Button( path_frame, text="Browse", command=self.browse_input ).pack(fill="x", pady=2) ttk.Label(path_frame, text="Output Folder").pack(anchor="w") ttk.Entry( path_frame, textvariable=self.output_var ).pack(fill="x") ttk.Button( path_frame, text="Browse", command=self.browse_output ).pack(fill="x", pady=2) # ================================================= # SINGLE IMAGE # ================================================= single_frame = ttk.LabelFrame(left, text="Single Image Expansion") single_frame.pack(fill="x", pady=5) ttk.Label(single_frame, text="Single Input Image").pack(anchor="w") ttk.Entry( single_frame, textvariable=self.single_image_var ).pack(fill="x") ttk.Button( single_frame, text="Browse Image", command=self.browse_single_image ).pack(fill="x", pady=2) ttk.Label( single_frame, text="Repeat Count" ).pack(anchor="w") repeat_box = ttk.Spinbox( single_frame, from_=1, to=1000000, textvariable=self.repeat_count_var ) repeat_box.pack(fill="x", pady=2) # ================================================= # PRESETS # ================================================= preset_frame = ttk.LabelFrame(left, text="Presets") preset_frame.pack(fill="x", pady=5) ttk.Button( preset_frame, text="Soft", command=self.apply_soft_preset ).pack(fill="x") ttk.Button( preset_frame, text="Medium", command=self.apply_medium_preset ).pack(fill="x") ttk.Button( preset_frame, text="Extreme", command=self.apply_extreme_preset ).pack(fill="x") ttk.Button( preset_frame, text="Save Preset", command=self.save_named_preset ).pack(fill="x", pady=3) ttk.Button( preset_frame, text="Load Preset", command=self.load_named_preset ).pack(fill="x") # ================================================= # GENERAL # ================================================= general = ttk.LabelFrame(left, text="General") general.pack(fill="x", pady=5) self.make_slider( general, "Strength", self.strength_var, 0.1, 5.0 ) ttk.Label(general, text="Mode").pack(anchor="w") ttk.Combobox( general, textvariable=self.mode_var, values=["rgb", "mono"], state="readonly" ).pack(fill="x", pady=3) # ================================================= # TOGGLES # ================================================= toggles = ttk.LabelFrame(left, text="Enable / Disable") toggles.pack(fill="x", pady=5) self.make_toggle(toggles, "Noise", self.enable_noise_var) self.make_toggle(toggles, "Blur", self.enable_blur_var) self.make_toggle(toggles, "Sharpen", self.enable_sharpen_var) self.make_toggle(toggles, "Rotate", self.enable_rotate_var) self.make_toggle(toggles, "Resize", self.enable_resize_var) self.make_toggle(toggles, "Roll", self.enable_roll_var) self.make_toggle(toggles, "RGB Shift", self.enable_rgb_shift_var) self.make_toggle(toggles, "Shapes", self.enable_shapes_var) self.make_toggle(toggles, "Preview", self.enable_preview_var) # ================================================= # EFFECTS # ================================================= effects = ttk.LabelFrame(left, text="Effects") effects.pack(fill="x", pady=5) self.make_slider(effects, "Noise Min", self.noise_min_var, 0.0, 10.0) self.make_slider(effects, "Noise Max", self.noise_max_var, 0.0, 10.0) self.make_slider(effects, "Rotate Min", self.rotate_min_var, -20.0, 0.0) self.make_slider(effects, "Rotate Max", self.rotate_max_var, 0.0, 20.0) self.make_slider(effects, "Blur Max", self.blur_max_var, 0.0, 10.0) self.make_slider(effects, "Sharpen Max", self.sharpen_max_var, 0.0, 20.0) self.make_slider(effects, "Quality Min", self.quality_min_var, 1, 100) self.make_slider(effects, "Quality Max", self.quality_max_var, 1, 100) self.make_slider(effects, "Roll Max", self.roll_max_var, 0, 50) self.make_slider(effects, "Resize Min %", self.resize_min_var, 50, 100) self.make_slider(effects, "Resize Max %", self.resize_max_var, 100, 150) # ================================================= # SHAPES # ================================================= shapes = ttk.LabelFrame(left, text="Shapes") shapes.pack(fill="x", pady=5) self.make_slider(shapes, "Shape Count", self.shape_count_var, 0, 50) self.make_slider(shapes, "Shape Size Min", self.shape_size_min_var, 1, 500) self.make_slider(shapes, "Shape Size Max", self.shape_size_max_var, 1, 1000) self.make_slider(shapes, "Alpha Min", self.shape_alpha_min_var, 0.0, 1.0) self.make_slider(shapes, "Alpha Max", self.shape_alpha_max_var, 0.0, 1.0) ttk.Button( shapes, text="Choose Fixed Shape Color", command=self.pick_shape_color ).pack(fill="x") # ================================================= # PROCESSING # ================================================= process_frame = ttk.LabelFrame(left, text="Processing") process_frame.pack(fill="x", pady=5) ttk.Button( process_frame, text="Preview First Image", command=self.preview_first_image ).pack(fill="x") ttk.Button( process_frame, text="RUN", command=self.start_processing ).pack(fill="x", pady=3) ttk.Button( process_frame, text="STOP", command=self.stop_processing ).pack(fill="x") self.progress = ttk.Progressbar( process_frame, orient="horizontal", mode="determinate" ) self.progress.pack(fill="x", pady=5) self.status = ttk.Label(process_frame, text="Idle") self.status.pack(anchor="w") # ================================================= # PREVIEW # ================================================= preview_frame = ttk.LabelFrame(right, text="Preview") preview_frame.pack(fill="both", expand=True) self.preview_label = ttk.Label(preview_frame) self.preview_label.pack(fill="both", expand=True) # ===================================================== # HELPERS # ===================================================== def make_slider(self, parent, label, variable, frm, to): container = ttk.Frame(parent) container.pack(fill="x", pady=2) top = ttk.Frame(container) top.pack(fill="x") ttk.Label( top, text=label ).pack(side="left") value_label = ttk.Label( top, text=str(variable.get()), width=6, anchor="e" ) value_label.pack(side="right") slider = ttk.Scale( container, variable=variable, from_=frm, to=to ) slider.pack(fill="x") def update_label(*args): value = variable.get() if isinstance(variable, tk.IntVar): value_label.config(text=str(int(value))) else: value_label.config(text=f"{float(value):.2f}") variable.trace_add("write", update_label) update_label() def make_toggle(self, parent, text, variable): ttk.Checkbutton( parent, text=text, variable=variable ).pack(anchor="w") # ===================================================== # BROWSERS # ===================================================== def browse_input(self): folder = filedialog.askdirectory() if folder: self.input_var.set(folder) def browse_output(self): folder = filedialog.askdirectory() if folder: self.output_var.set(folder) def browse_single_image(self): file = filedialog.askopenfilename( filetypes=[("Images", "*.jpg *.jpeg *.png *.webp")] ) if file: self.single_image_var.set(file) # ===================================================== # PRESETS # ===================================================== def apply_soft_preset(self): self.strength_var.set(0.7) self.noise_max_var.set(1.0) self.blur_max_var.set(0.2) self.roll_max_var.set(3) def apply_medium_preset(self): self.strength_var.set(1.0) self.noise_max_var.set(3.0) self.blur_max_var.set(0.7) self.roll_max_var.set(8) def apply_extreme_preset(self): self.strength_var.set(3.0) self.noise_max_var.set(8.0) self.blur_max_var.set(5.0) self.roll_max_var.set(40) def save_named_preset(self): name = filedialog.asksaveasfilename( defaultextension=".json", initialdir=PRESET_DIR, filetypes=[("JSON", "*.json")] ) if not name: return data = {} for key, value in self.__dict__.items(): if isinstance(value, tk.Variable): data[key] = value.get() with open(name, "w") as f: json.dump(data, f, indent=4) messagebox.showinfo("Saved", "Preset saved") def load_named_preset(self): name = filedialog.askopenfilename( initialdir=PRESET_DIR, filetypes=[("JSON", "*.json")] ) if not name: return with open(name, "r") as f: data = json.load(f) for key, val in data.items(): if hasattr(self, key): var = getattr(self, key) if isinstance(var, tk.Variable): var.set(val) messagebox.showinfo("Loaded", "Preset loaded") # ===================================================== # COLOR # ===================================================== def pick_shape_color(self): color = colorchooser.askcolor()[0] if color: self.custom_shape_color = tuple(map(int, color)) # ===================================================== # COLLECT # ===================================================== def collect_images(self): images = [] if self.single_image_var.get(): img = Path(self.single_image_var.get()) repeat_count = self.repeat_count_var.get() for i in range(repeat_count): images.append((img, i + 1)) return images input_dir = Path(self.input_var.get()) for ext in SUPPORTED_EXTENSIONS: for file in input_dir.glob(ext): images.append((file, None)) return images # ===================================================== # PREVIEW # ===================================================== def preview_first_image(self): images = self.collect_images() if not images: messagebox.showerror("Error", "No images found") return img, _ = images[0] preview_dir = Path("preview_temp") preview_dir.mkdir(exist_ok=True) out = preview_dir / "preview.jpg" self.process_single_image(img, out) self.show_preview(out) def show_preview(self, path): try: img = Image.open(path) img.thumbnail((300, 300)) if hasattr(self, "preview_photo"): del self.preview_photo self.preview_photo = ImageTk.PhotoImage(img) self.preview_label.configure(image=self.preview_photo) except Exception as e: print(e) # ===================================================== # THREAD # ===================================================== def start_processing(self): if self.processing: return self.stop_requested = False thread = threading.Thread( target=self.process_all_images, daemon=True ) thread.start() def stop_processing(self): self.stop_requested = True # ===================================================== # PROCESS ALL # ===================================================== def process_all_images(self): self.processing = True try: images = self.collect_images() if not images: self.root.after( 0, lambda: messagebox.showerror( "Error", "No images found" ) ) return if not self.output_var.get(): self.root.after( 0, lambda: messagebox.showerror( "Error", "Please choose output folder" ) ) return output_dir = Path(self.output_var.get()) output_dir.mkdir(exist_ok=True) total = len(images) self.root.after( 0, lambda: self.progress.configure(maximum=total) ) for i, (img, duplicate_index) in enumerate(images): if self.stop_requested: break if duplicate_index is not None: stem = img.stem ext = img.suffix filename = f"{stem}_{duplicate_index:04d}{ext}" else: filename = img.name out = output_dir / filename self.root.after( 0, lambda f=filename, c=i + 1: self.status.config( text=f"Processing {c}/{total}: {f}" ) ) self.process_single_image(img, out) self.root.after( 0, lambda v=i + 1: self.progress.configure(value=v) ) if self.enable_preview_var.get(): if i % 10 == 0: self.root.after( 0, lambda p=out: self.show_preview(p) ) if self.stop_requested: self.root.after( 0, lambda: self.status.config( text="Stopped" ) ) else: self.root.after( 0, lambda: self.status.config( text="Finished" ) ) self.root.after( 0, lambda: messagebox.showinfo( "Done", f"Processed {total} images" ) ) except Exception as e: self.root.after( 0, lambda: messagebox.showerror( "Processing Error", str(e) ) ) finally: self.processing = False # ===================================================== # PROCESS SINGLE # ===================================================== def process_single_image(self, img, out): with Image.open(img) as im: width, height = im.size strength = self.strength_var.get() noise = round( random.uniform( self.noise_min_var.get(), self.noise_max_var.get() ) * strength, 3 ) rotate = round( random.uniform( self.rotate_min_var.get(), self.rotate_max_var.get() ) * strength, 3 ) quality = random.randint( self.quality_min_var.get(), self.quality_max_var.get() ) blur = round( random.uniform(0.0, self.blur_max_var.get()) * strength, 3 ) sharpen = round( random.uniform(0.0, self.sharpen_max_var.get()) * strength, 3 ) resize = round( random.uniform( self.resize_min_var.get(), self.resize_max_var.get() ), 3 ) rollx = random.randint( -self.roll_max_var.get(), self.roll_max_var.get() ) rolly = random.randint( -self.roll_max_var.get(), self.roll_max_var.get() ) brightness = random.randint(-4, 4) contrast = random.randint(-4, 4) gamma = round(random.uniform(0.94, 1.06), 3) channel_shift_r = random.randint(-3, 3) channel_shift_g = random.randint(-3, 3) channel_shift_b = random.randint(-3, 3) cmd = [ "magick", str(img), "-strip", ] if self.enable_resize_var.get(): cmd += [ "-filter", "Lanczos", "-resize", f"{resize}%" ] if self.enable_rotate_var.get(): cmd += [ "-virtual-pixel", "mirror", "-distort", "SRT", str(rotate) ] cmd += [ "-brightness-contrast", f"{brightness}x{contrast}", "-gamma", str(gamma), ] if self.enable_blur_var.get(): cmd += ["-blur", f"0x{blur}"] if self.enable_sharpen_var.get(): cmd += ["-sharpen", f"0x{sharpen}"] if self.enable_roll_var.get(): cmd += ["-roll", f"{rollx}x{rolly}"] if self.mode_var.get() == "rgb" and self.enable_rgb_shift_var.get(): cmd += [ "-channel", "R", "-roll", f"{channel_shift_r}x0", "-channel", "G", "-roll", f"{channel_shift_g}x0", "-channel", "B", "-roll", f"{channel_shift_b}x0", "-channel", "RGB" ] if self.enable_noise_var.get(): cmd += [ "-attenuate", str(noise), "+noise", "Gaussian", ] if self.mode_var.get() == "mono": cmd += [ "-colorspace", "Gray", "-colorspace", "sRGB" ] if self.enable_shapes_var.get(): for _ in range(self.shape_count_var.get()): shape_type = random.choice([ "circle", "rectangle", "triangle", "ellipse" ]) size = random.randint( self.shape_size_min_var.get(), self.shape_size_max_var.get() ) x = random.randint(0, width) y = random.randint(0, height) if self.custom_shape_color: r, g, b = self.custom_shape_color else: r = random.randint(0, 255) g = random.randint(0, 255) b = random.randint(0, 255) alpha = round( random.uniform( self.shape_alpha_min_var.get(), self.shape_alpha_max_var.get() ), 2 ) color = f"rgba({r},{g},{b},{alpha})" if shape_type == "circle": draw = ( f"fill '{color}' " f"circle {x},{y} {x + size},{y}" ) elif shape_type == "rectangle": draw = ( f"fill '{color}' " f"rectangle " f"{x},{y} " f"{x + size},{y + size}" ) elif shape_type == "ellipse": draw = ( f"fill '{color}' " f"ellipse " f"{x},{y} " f"{size},{size // 2} " f"0,360" ) else: x2 = x + size y2 = y + size x3 = x - size // 2 y3 = y + size draw = ( f"fill '{color}' " f"polygon " f"{x},{y} " f"{x2},{y2} " f"{x3},{y3}" ) cmd += ["-draw", draw] cmd += [ "-quality", str(quality), str(out) ] result = subprocess.run( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) if result.returncode != 0: print(result.stderr) else: print("SUCCESS:", out) # ========================================================= # RUN # ========================================================= if __name__ == "__main__": root = tk.Tk() app = RandomizerApp(root) root.mainloop()