フォトモザイクメーカー『欠片幻彩』最新版 ソースコード

『欠片幻彩』は、指定したフォルダ内の画像を読み込み、縮小して並べることで、モザイクアート風の1枚画像を作成するツールです。

>>GUI外観 >>使い方

以下は『欠片幻彩』最新版 のソースコード全文です:

 
        import os
        import random
        from datetime import datetime
        import tkinter as tk
        from tkinter import filedialog, colorchooser, messagebox
        from PIL import Image, ImageTk, ImageOps
        
        
        class ToolTip:
            def __init__(self, widget, text=""):
                self.widget = widget
                self.text = text
                self.tip_window = None
                widget.bind("<Enter>", self.show_tip)
                widget.bind("<Leave>", self.hide_tip)
        
            def show_tip(self, event=None):
                if self.tip_window or not self.text:
                    return
                x = self.widget.winfo_rootx() + 20
                y = self.widget.winfo_rooty() + 20
                self.tip_window = tw = tk.Toplevel(self.widget)
                tw.wm_overrideredirect(True)
                tw.wm_geometry(f"+{x}+{y}")
                label = tk.Label(
                    tw,
                    text=self.text,
                    justify="left",
                    background="#ffffe0",
                    relief="solid",
                    borderwidth=1,
                    font=("Noto Sans JP", 9)
                )
                label.pack(ipadx=4, ipady=2)
        
            def hide_tip(self, event=None):
                if self.tip_window:
                    self.tip_window.destroy()
                    self.tip_window = None
        
        
        class MosaicApp:
            def __init__(self, root):
                self.root = root
                self.root.title("フォトモザイクメーカー - 欠片幻彩")
        
                self.width_var = tk.IntVar(value=1920)
                self.height_var = tk.IntVar(value=1080)
                self.scale_var = tk.DoubleVar(value=0.1)
                self.bg_color = "#ffffff"
                self.transparent_bg_var = tk.BooleanVar(value=False)
                self.folder_path = ""
                self.generated_img = None
                self.format_var = tk.StringVar(value="PNG")
        
                self.arrangement_var = tk.StringVar(value="random")
                self.sort_order_var = tk.StringVar(value="asc")
                self.same_size_crop_var = tk.BooleanVar(value=False)
                self.crop_basis_var = tk.StringVar(value="first")
                self.crop_width_var = tk.IntVar(value=200)
                self.crop_height_var = tk.IntVar(value=200)
        
                main_frame = tk.Frame(root, padx=10, pady=10)
                main_frame.pack(fill="both", expand=True)
        
                left_frame = tk.LabelFrame(main_frame, text="設定", padx=10, pady=10)
                left_frame.pack(side="left", fill="y")
        
                tk.Label(left_frame, text="画像サイズ:").grid(row=0, column=0, sticky="w")
                tk.Entry(left_frame, textvariable=self.width_var, width=6).grid(row=0, column=1)
                tk.Label(left_frame, text="x").grid(row=0, column=2)
                tk.Entry(left_frame, textvariable=self.height_var, width=6).grid(row=0, column=3)
        
                self.bg_label = tk.Label(left_frame, text="背景色:")
                self.bg_label.grid(row=1, column=0, sticky="w")
        
                self.bg_button = tk.Button(left_frame, text="選択", command=self.choose_color)
                self.bg_button.grid(row=1, column=1, sticky="w")
        
                self.color_preview = tk.Label(left_frame, bg=self.bg_color, width=4, relief="sunken")
                self.color_preview.grid(row=1, column=2, columnspan=2, sticky="w")
        
                self.transparent_check = tk.Checkbutton(
                    left_frame,
                    text="背景を透明に",
                    variable=self.transparent_bg_var,
                    command=self.update_bg_controls
                )
                self.transparent_check.grid(row=2, column=0, columnspan=4, sticky="w", pady=(2, 5))
        
                tk.Button(left_frame, text="画像フォルダ選択", command=self.choose_folder).grid(
                    row=3, column=0, columnspan=4, pady=(5, 0), sticky="w"
                )
                self.folder_label = tk.Label(
                    left_frame, text="未選択", wraplength=220, fg="gray", anchor="w", justify="left"
                )
                self.folder_label.grid(row=4, column=0, columnspan=4, sticky="w", pady=(2, 5))
                self.folder_tooltip = ToolTip(self.folder_label, "")
        
                tk.Label(left_frame, text="縮小率:").grid(row=5, column=0, sticky="w")
                tk.Entry(left_frame, textvariable=self.scale_var, width=6).grid(row=5, column=1, sticky="w")
        
                tk.Label(left_frame, text="画像群の配列:").grid(row=6, column=0, sticky="w")
                self.arrangement_random_radio = tk.Radiobutton(
                    left_frame,
                    text="ランダム",
                    variable=self.arrangement_var,
                    value="random",
                    command=self.update_arrangement_controls
                )
                self.arrangement_random_radio.grid(row=6, column=1, sticky="w")
        
                self.arrangement_aligned_radio = tk.Radiobutton(
                    left_frame,
                    text="整列",
                    variable=self.arrangement_var,
                    value="aligned",
                    command=self.update_arrangement_controls
                )
                self.arrangement_aligned_radio.grid(row=6, column=2, sticky="w")
        
                self.sort_order_label = tk.Label(left_frame, text="整列順:")
                self.sort_order_label.grid(row=7, column=0, sticky="w")
        
                self.sort_asc_radio = tk.Radiobutton(
                    left_frame,
                    text="ファイル名昇順",
                    variable=self.sort_order_var,
                    value="asc"
                )
                self.sort_asc_radio.grid(row=7, column=1, sticky="w")
        
                self.sort_desc_radio = tk.Radiobutton(
                    left_frame,
                    text="ファイル名降順",
                    variable=self.sort_order_var,
                    value="desc"
                )
                self.sort_desc_radio.grid(row=7, column=2, sticky="w")
        
                self.sort_random_radio = tk.Radiobutton(
                    left_frame,
                    text="ランダム",
                    variable=self.sort_order_var,
                    value="random"
                )
                self.sort_random_radio.grid(row=7, column=3, sticky="w")
        
                self.same_size_crop_check = tk.Checkbutton(
                    left_frame,
                    text="同一サイズでトリミング",
                    variable=self.same_size_crop_var,
                    command=self.update_arrangement_controls
                )
                self.same_size_crop_check.grid(row=8, column=0, columnspan=4, sticky="w", pady=(2, 2))
        
                self.crop_basis_label = tk.Label(left_frame, text="トリミング基準:")
                self.crop_basis_label.grid(row=9, column=0, sticky="w")
        
                self.crop_basis_first_radio = tk.Radiobutton(
                    left_frame,
                    text="先頭画像基準",
                    variable=self.crop_basis_var,
                    value="first",
                    command=self.update_arrangement_controls
                )
                self.crop_basis_first_radio.grid(row=9, column=1, sticky="w")
        
                self.crop_basis_min_radio = tk.Radiobutton(
                    left_frame,
                    text="最小画像基準",
                    variable=self.crop_basis_var,
                    value="min",
                    command=self.update_arrangement_controls
                )
                self.crop_basis_min_radio.grid(row=9, column=2, sticky="w")
        
                self.crop_basis_max_radio = tk.Radiobutton(
                    left_frame,
                    text="最大画像基準",
                    variable=self.crop_basis_var,
                    value="max",
                    command=self.update_arrangement_controls
                )
                self.crop_basis_max_radio.grid(row=9, column=3, sticky="w")
        
                self.crop_basis_custom_radio = tk.Radiobutton(
                    left_frame,
                    text="指定サイズ",
                    variable=self.crop_basis_var,
                    value="custom",
                    command=self.update_arrangement_controls
                )
                self.crop_basis_custom_radio.grid(row=10, column=1, sticky="w")
        
                self.crop_size_label = tk.Label(left_frame, text="指定サイズ:")
                self.crop_size_label.grid(row=10, column=2, sticky="e")
        
                self.crop_width_entry = tk.Entry(left_frame, textvariable=self.crop_width_var, width=6)
                self.crop_width_entry.grid(row=10, column=3, sticky="w")
        
                self.crop_size_x_label = tk.Label(left_frame, text="x")
                self.crop_size_x_label.grid(row=10, column=3, padx=(48, 0), sticky="w")
        
                self.crop_height_entry = tk.Entry(left_frame, textvariable=self.crop_height_var, width=6)
                self.crop_height_entry.grid(row=10, column=3, padx=(68, 0), sticky="w")
        
                tk.Label(left_frame, text="保存形式:").grid(row=11, column=0, sticky="w")
                format_menu = tk.OptionMenu(left_frame, self.format_var, "PNG", "JPG")
                format_menu.config(width=8)
                format_menu.grid(row=11, column=1, sticky="w")
        
                tk.Button(left_frame, text="生成", command=self.generate, width=10).grid(
                    row=12, column=0, columnspan=2, pady=5
                )
                tk.Button(left_frame, text="保存", command=self.save, width=10).grid(
                    row=12, column=2, columnspan=2, pady=5
                )
        
                right_frame = tk.LabelFrame(main_frame, text="プレビュー", padx=10, pady=10)
                right_frame.pack(side="right", fill="both", expand=True)
        
                self.preview_label = tk.Label(right_frame)
                self.preview_label.pack(expand=True)
        
                bottom_frame = tk.Frame(root)
                bottom_frame.pack(side="bottom", pady=(0, 6))
        
                version_label = tk.Label(bottom_frame, text="ver.3.0.4", fg="grey", font=("Noto Sans JP", 8))
                version_label.pack(side="left")
        
                help_label = tk.Label(
                    bottom_frame,
                    text=" ヘルプ",
                    fg="blue",
                    cursor="hand2",
                    font=("Noto Sans JP", 8, "underline")
                )
                help_label.pack(side="left")
                help_label.bind("<Button-1>", self.show_help)
        
                self.update_bg_controls()
                self.update_arrangement_controls()
        
            def choose_color(self):
                if self.transparent_bg_var.get():
                    return
                color = colorchooser.askcolor(title="背景色を選択")
                if color[1]:
                    self.bg_color = color[1]
                    self.color_preview.config(bg=self.bg_color)
        
            def update_bg_controls(self):
                if self.transparent_bg_var.get():
                    self.bg_label.config(fg="gray")
                    self.bg_button.config(state="disabled")
                    self.color_preview.config(bg="#d9d9d9")
                else:
                    self.bg_label.config(fg="black")
                    self.bg_button.config(state="normal")
                    self.color_preview.config(bg=self.bg_color)
        
            def update_arrangement_controls(self):
                is_aligned = self.arrangement_var.get() == "aligned"
                state_aligned = "normal" if is_aligned else "disabled"
                fg_aligned = "black" if is_aligned else "gray"
        
                self.sort_order_label.config(fg=fg_aligned)
                self.sort_asc_radio.config(state=state_aligned)
                self.sort_desc_radio.config(state=state_aligned)
                self.sort_random_radio.config(state=state_aligned)
                self.same_size_crop_check.config(state=state_aligned)
        
                is_crop_enabled = is_aligned and self.same_size_crop_var.get()
                state_crop = "normal" if is_crop_enabled else "disabled"
                fg_crop = "black" if is_crop_enabled else "gray"
        
                self.crop_basis_label.config(fg=fg_crop)
                self.crop_basis_first_radio.config(state=state_crop)
                self.crop_basis_min_radio.config(state=state_crop)
                self.crop_basis_max_radio.config(state=state_crop)
                self.crop_basis_custom_radio.config(state=state_crop)
        
                is_custom = is_crop_enabled and self.crop_basis_var.get() == "custom"
                state_custom = "normal" if is_custom else "disabled"
                fg_custom = "black" if is_custom else "gray"
        
                self.crop_size_label.config(fg=fg_custom)
                self.crop_size_x_label.config(fg=fg_custom)
                self.crop_width_entry.config(state=state_custom)
                self.crop_height_entry.config(state=state_custom)
        
            def choose_folder(self):
                self.folder_path = filedialog.askdirectory(title="画像フォルダを選択")
                if self.folder_path:
                    self.folder_label.config(text=self.folder_path, fg="black")
                    self.folder_tooltip.text = self.folder_path
                else:
                    self.folder_label.config(text="未選択", fg="gray")
                    self.folder_tooltip.text = ""
        
            def get_image_files(self):
                return [
                    f for f in os.listdir(self.folder_path)
                    if f.lower().endswith((".png", ".jpg", ".jpeg", ".bmp"))
                ]
        
            def open_and_scale_image(self, img_path, scale):
                img = Image.open(img_path)
                img = ImageOps.exif_transpose(img).convert("RGBA")
                new_w = max(1, int(img.width * scale))
                new_h = max(1, int(img.height * scale))
                return img.resize((new_w, new_h))
        
            def fit_crop_center(self, img, target_w, target_h):
                src_w, src_h = img.size
                if src_w <= 0 or src_h <= 0 or target_w <= 0 or target_h <= 0:
                    return img
        
                scale = max(target_w / src_w, target_h / src_h)
                resized_w = max(1, int(src_w * scale))
                resized_h = max(1, int(src_h * scale))
                img = img.resize((resized_w, resized_h))
        
                left = max(0, (resized_w - target_w) // 2)
                top = max(0, (resized_h - target_h) // 2)
                right = left + target_w
                bottom = top + target_h
        
                return img.crop((left, top, right, bottom))
        
            def get_scaled_sizes(self, files, scale):
                sizes = []
                for file in files:
                    img_path = os.path.join(self.folder_path, file)
                    try:
                        with Image.open(img_path) as img:
                            img = ImageOps.exif_transpose(img)
                            new_w = max(1, int(img.width * scale))
                            new_h = max(1, int(img.height * scale))
                            sizes.append((file, new_w, new_h))
                    except Exception:
                        continue
                return sizes
        
            def determine_crop_target_size(self, files, scale):
                basis = self.crop_basis_var.get()
        
                if basis == "custom":
                    custom_w = self.crop_width_var.get()
                    custom_h = self.crop_height_var.get()
                    if custom_w <= 0 or custom_h <= 0:
                        return None
                    return (custom_w, custom_h)
        
                sizes = self.get_scaled_sizes(files, scale)
                if not sizes:
                    return None
        
                if basis == "first":
                    _, w, h = sizes[0]
                    return (w, h)
                elif basis == "min":
                    min_w = min(w for _, w, _ in sizes)
                    min_h = min(h for _, _, h in sizes)
                    return (min_w, min_h)
                elif basis == "max":
                    max_w = max(w for _, w, _ in sizes)
                    max_h = max(h for _, _, h in sizes)
                    return (max_w, max_h)
        
                return None
        
            def get_image_for_jpeg(self):
                if self.generated_img.mode == "RGB":
                    return self.generated_img
        
                if self.transparent_bg_var.get():
                    white_bg = Image.new("RGB", self.generated_img.size, (255, 255, 255))
                    white_bg.paste(self.generated_img, mask=self.generated_img.getchannel("A"))
                    return white_bg
        
                return self.generated_img.convert("RGB")
        
            def generate(self):
                if not self.folder_path:
                    messagebox.showwarning("警告", "画像フォルダを選択してください。")
                    return
        
                W, H = self.width_var.get(), self.height_var.get()
                scale = self.scale_var.get()
        
                if W <= 0 or H <= 0:
                    messagebox.showwarning("警告", "画像サイズは1以上を指定してください。")
                    return
        
                if scale <= 0:
                    messagebox.showwarning("警告", "縮小率は0より大きい値を指定してください。")
                    return
        
                if self.arrangement_var.get() == "aligned" and self.same_size_crop_var.get():
                    if self.crop_basis_var.get() == "custom":
                        if self.crop_width_var.get() <= 0 or self.crop_height_var.get() <= 0:
                            messagebox.showwarning("警告", "指定サイズは1以上を指定してください。")
                            return
        
                if self.transparent_bg_var.get():
                    mosaic = Image.new("RGBA", (W, H), (0, 0, 0, 0))
                else:
                    mosaic = Image.new("RGBA", (W, H), self.bg_color)
        
                files = self.get_image_files()
                if not files:
                    messagebox.showwarning("警告", "フォルダに画像がありません。")
                    return
        
                arrangement = self.arrangement_var.get()
        
                if arrangement == "random":
                    random.shuffle(files)
        
                    for file in files:
                        img_path = os.path.join(self.folder_path, file)
                        try:
                            img = self.open_and_scale_image(img_path, scale)
                        except Exception:
                            continue
        
                        x = random.randint(0, max(0, W - img.width))
                        y = random.randint(0, max(0, H - img.height))
                        mosaic.alpha_composite(img, (x, y))
        
                else:
                    sort_mode = self.sort_order_var.get()
                    if sort_mode == "asc":
                        files = sorted(files, key=lambda x: x.lower())
                    elif sort_mode == "desc":
                        files = sorted(files, key=lambda x: x.lower(), reverse=True)
                    else:
                        random.shuffle(files)
        
                    target_size = None
                    if self.same_size_crop_var.get():
                        target_size = self.determine_crop_target_size(files, scale)
                        if target_size is None:
                            messagebox.showwarning("警告", "トリミング基準サイズを決定できませんでした。")
                            return
        
                    x, y = 0, 0
                    row_height = 0
        
                    for file in files:
                        img_path = os.path.join(self.folder_path, file)
                        try:
                            img = self.open_and_scale_image(img_path, scale)
                            if self.same_size_crop_var.get():
                                img = self.fit_crop_center(img, target_size[0], target_size[1])
                        except Exception:
                            continue
        
                        if x + img.width > W:
                            x = 0
                            y += row_height
                            row_height = 0
        
                        if y + img.height > H:
                            break
        
                        mosaic.alpha_composite(img, (x, y))
                        x += img.width
                        row_height = max(row_height, img.height)
        
                self.generated_img = mosaic
        
                preview = mosaic.copy()
                preview_bg = Image.new("RGBA", preview.size, (255, 255, 255, 255))
                preview = Image.alpha_composite(preview_bg, preview).convert("RGB")
                preview.thumbnail((500, 500))
                self.tk_img = ImageTk.PhotoImage(preview)
                self.preview_label.config(image=self.tk_img)
        
            def save(self):
                if self.generated_img is None:
                    messagebox.showwarning("警告", "まず画像を生成してください。")
                    return
        
                selected_format = self.format_var.get().upper()
        
                if self.transparent_bg_var.get() and selected_format == "JPG":
                    result = messagebox.askyesno(
                        "確認",
                        "JPEGは透明背景を保存できません。\n"
                        "透明部分は白背景で保存されます。\n\n"
                        "このままJPEGで保存しますか?"
                    )
                    if not result:
                        return
        
                extension = ".png" if selected_format == "PNG" else ".jpg"
                default_name = datetime.now().strftime("%Y%m%d_%H%M%S") + extension
        
                filetypes = [
                    ("PNG画像", "*.png"),
                    ("JPEG画像", "*.jpg"),
                    ("すべてのファイル", "*.*")
                ]
        
                path = filedialog.asksaveasfilename(
                    title="保存先とファイル名を指定",
                    defaultextension=extension,
                    initialfile=default_name,
                    filetypes=filetypes
                )
        
                if not path:
                    return
        
                base, ext = os.path.splitext(path)
                ext = ext.lower()
        
                expected_ext = ".png" if selected_format == "PNG" else ".jpg"
                valid_exts = (".png",) if selected_format == "PNG" else (".jpg", ".jpeg")
                if ext not in valid_exts:
                    path = base + expected_ext
        
                try:
                    if selected_format == "PNG":
                        self.generated_img.save(path, format="PNG")
                    else:
                        rgb_img = self.get_image_for_jpeg()
                        rgb_img.save(path, format="JPEG", quality=95)
        
                    messagebox.showinfo("保存完了", f"保存しました:\n{path}")
                except Exception as e:
                    messagebox.showerror("保存エラー", f"保存に失敗しました。\n{e}")
        
            def show_help(self, event=None):
                help_window = tk.Toplevel(self.root)
                help_window.title("ヘルプ")
                help_window.resizable(False, False)
                help_window.transient(self.root)
                help_window.grab_set()
        
                message = (
                    "【使い方】\n"
                    "1. 画像サイズを指定します。\n"
                    "2. 背景色を選ぶか、「背景を透明に」をチェックします。\n"
                    "3. 画像フォルダを選択します。\n"
                    "4. 縮小率を指定し、配列方法を選んで「生成」を押します。\n"
                    "5. 保存形式を選び、「保存」を押して保存先とファイル名を指定します。\n\n"
                    "【補足】\n"
                    "・PNGは透明背景に対応しています。\n"
                    "・JPEGは透明背景に対応していないため、透明部分は白で保存されます。\n"
                    "・「ランダム」はランダム座標に配置します。\n"
                    "・「整列」は左上から順に並べます。\n"
                    "・整列順は「ファイル名昇順」「ファイル名降順」「ランダム」から選べます。\n"
                    "・「同一サイズでトリミング」をONにすると、各画像を同じサイズに中央トリミングして整列します。\n"
                    "・トリミング基準は「先頭画像基準」「最小画像基準」「最大画像基準」「指定サイズ」から選べます。"
                )
        
                tk.Label(
                    help_window,
                    text=message,
                    justify="left",
                    anchor="w",
                    padx=12,
                    pady=12,
                    wraplength=420
                ).pack()
        
                tk.Button(help_window, text="閉じる", command=help_window.destroy, width=10).pack(pady=(0, 12))
        
        
        if __name__ == "__main__":
            root = tk.Tk()
        
            # タイトルバーアイコン表示用
            data = '''R0lGODlhEAAQAIYAAAAAAEJYafpzWVJVVvNqTkW36j5UY/8AAP9/AKpVVVWq/+5pU///APtxWD2q2fFxUgB/f0Gq20hhc/ZuVENVZUBWZv9/f/9VVeRrUOdFRfxpTMFmWNpkTft1Wn//////f3ubsX9///9yTknB9vtyV0W26UKx40ey4P98YUCr2/WTSEeo01Wqqv+qVVVVqv+yM0JbbPi5O0FWZv/EOkFSYv/KPi2y6j+////KPjpYai5SZj1VZDxUZP//Mz9VVT9OXSRISD8/fz8/SwD//3VbXwZdhhxKYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAQAi0AAEIHEgQgAeCAwAYCMAwAAUOGlAIEEDggAUAQQBQkCDDB4AIBUZEAHCjYEIjGyY+wFHjBYIDBQEkBMBCoAMFBYE0DPDjRAEbAhMgBFBBAo8GAh4saEAAwMWCEAoyiClwpsAQVAXSMCDQBQAHJVIMwVmVKEMYQkwUKLACAdmEC3cWAUGABAYACMruYKhDxIQFEwUsAHBBYNQBOYhMmKgiBoAWMYUS6JChx4ysAxNcOMDgQ8GAADs='''
            root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
        
            app = MosaicApp(root)
            root.mainloop()
    

📦 ダウンロード 🏠 ホームへ