好きな場所で分割できる画像分割ツール バッチ処理対応

イメージ

プログラム

import os
import json
from tkinter import *
from tkinter import filedialog
from PIL import Image, ImageTk

class ImageSplitterGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("画像分割ツール")

        self.folder = ""
        self.image_path = ""
        self.image = None
        self.zoomed_image = None
        self.tk_image = None

        self.split_data = {"vertical": [], "horizontal": []}

        self.mode_var = StringVar(value="vertical")

        self.dragging = False
        self.drag_line_type = None
        self.drag_index = None

        self.temp_results = []

        # ★ ズーム倍率(初期値100%)
        self.zoom_percent = 100

        self.setup_ui()

    # ---------------- UI ----------------
    def setup_ui(self):
        self.root.configure(bg="#f0f0f0")

        main = Frame(self.root, bg="#f0f0f0")
        main.pack(fill=BOTH, expand=True)

        # 左:サムネイル
        left = Frame(main, bg="#e0e0e0", bd=2, relief=GROOVE)
        left.pack(side=LEFT, fill=Y)

        Button(left, text="フォルダ選択", command=self.select_folder,
               font=("Meiryo", 11), bg="#ffffff").pack(fill=X, padx=5, pady=5)

        thumb_container = Frame(left, bg="#e0e0e0")
        thumb_container.pack(fill=BOTH, expand=True, padx=5, pady=5)

        self.thumb_canvas = Canvas(thumb_container, bg="#d0d0d0", width=150, bd=2, relief=SOLID)
        self.thumb_canvas.pack(side=LEFT, fill=BOTH, expand=True)

        thumb_scroll = Scrollbar(thumb_container, orient=VERTICAL, command=self.thumb_canvas.yview)
        thumb_scroll.pack(side=RIGHT, fill=Y)

        self.thumb_canvas.configure(yscrollcommand=thumb_scroll.set)
        self.thumb_inner = Frame(self.thumb_canvas, bg="#d0d0d0")
        self.thumb_canvas.create_window((0, 0), window=self.thumb_inner, anchor="nw")
        self.thumb_inner.bind("<Configure>", lambda e: self.thumb_canvas.configure(
            scrollregion=self.thumb_canvas.bbox("all")
        ))

        # 中央:プレビュー
        center = Frame(main, bg="#f0f0f0")
        center.pack(side=LEFT, fill=BOTH, expand=True)

        # ★ 情報表示(1行)
        self.info_label = Label(
            center,
            text="Size: - x -    Click: X=- Y=-    Zoom: 100%",
            bg="#f0f0f0",
            font=("Meiryo", 12)
        )
        self.info_label.pack(pady=5)

        preview_container = Frame(center, bg="#f0f0f0")
        preview_container.pack(fill=BOTH, expand=True)

        self.preview_canvas = Canvas(preview_container, bg="gray", bd=2, relief=SOLID)
        self.preview_canvas.pack(side=LEFT, fill=BOTH, expand=True)

        v_scroll = Scrollbar(preview_container, orient=VERTICAL, command=self.preview_canvas.yview)
        v_scroll.pack(side=RIGHT, fill=Y)

        h_scroll = Scrollbar(center, orient=HORIZONTAL, command=self.preview_canvas.xview)
        h_scroll.pack(fill=X)

        self.preview_canvas.configure(
            yscrollcommand=v_scroll.set,
            xscrollcommand=h_scroll.set
        )

        # 内部キャンバス
        self.canvas = Canvas(self.preview_canvas, bg="gray")
        self.canvas_id = self.preview_canvas.create_window((0, 0), window=self.canvas, anchor="nw")

        self.canvas.bind("<Configure>", self.update_scrollregion)

        # イベント
        self.canvas.bind("<Button-1>", self.on_click)
        self.canvas.bind("<ButtonPress-3>", self.on_drag_start)
        self.canvas.bind("<B3-Motion>", self.on_drag_move)
        self.canvas.bind("<ButtonRelease-3>", self.on_drag_end)

        # ボタン類
        btn_frame = Frame(center, bg="#f0f0f0")
        btn_frame.pack(pady=5)

        btn_style = {"font": ("Meiryo", 13), "width": 12, "height": 2}

        Radiobutton(btn_frame, text="縦線モード", variable=self.mode_var,
           value="vertical", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=0, padx=4)

        Radiobutton(btn_frame, text="横線モード", variable=self.mode_var,
           value="horizontal", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=1, padx=4)

        Radiobutton(btn_frame, text="縦横モード", variable=self.mode_var,
           value="both", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=2, padx=4)

        Button(btn_frame, text="キャンセル(最後)", command=self.undo_last, **btn_style).grid(row=0, column=3, padx=4)
        Button(btn_frame, text="全てクリア", command=self.reset_lines, **btn_style).grid(row=0, column=4, padx=4)

        Button(btn_frame, text="縦に分割", command=self.split_vertical, **btn_style).grid(row=1, column=0, padx=4)
        Button(btn_frame, text="横に分割", command=self.split_horizontal, **btn_style).grid(row=1, column=1, padx=4)
        Button(btn_frame, text="縦横に分割", command=self.split_both, **btn_style).grid(row=1, column=2, padx=4)
        Button(btn_frame, text="分割結果保存", command=self.save_split_results, **btn_style).grid(row=1, column=3, padx=4)
        Button(btn_frame, text="線情報保存", command=self.save_split_data, **btn_style).grid(row=1, column=4, padx=4)

        Button(btn_frame, text="線情報読み込み", command=self.load_split_data, **btn_style).grid(row=2, column=0, padx=4)
        # ★ バッチ処理ボタン(フォルダー内の全画像を自動分割)
        Button(btn_frame, text="バッチ処理", command=self.batch_process, **btn_style).grid(row=2, column=1, padx=4)

        # ★ ズームスライダー
        # ★ ZOOM + 微調整(1行にまとめる)
        zoom_adjust_frame = Frame(center, bg="#f0f0f0")
        zoom_adjust_frame.pack(pady=10)

        # ZOOM ラベル
        Label(zoom_adjust_frame, text="Zoom", bg="#f0f0f0", font=("Meiryo", 12)).grid(row=0, column=0, padx=5)

        # ZOOM スライダー
        self.zoom_slider = Scale(
            zoom_adjust_frame,
            from_=10, to=300,
            orient=HORIZONTAL,
            length=200,
            command=self.on_zoom_slider
        )
        self.zoom_slider.set(100)
        self.zoom_slider.grid(row=0, column=1, padx=10)

        # 微調整ボタン(← → ↑ ↓)
        Button(zoom_adjust_frame, text="←", width=4, command=lambda: self.adjust_last_line(-1)).grid(row=0, column=2)
        Button(zoom_adjust_frame, text="→", width=4, command=lambda: self.adjust_last_line(1)).grid(row=0, column=3)
        Button(zoom_adjust_frame, text="↑", width=4, command=lambda: self.adjust_last_line(-1, vertical=False)).grid(row=0, column=4)
        Button(zoom_adjust_frame, text="↓", width=4, command=lambda: self.adjust_last_line(1, vertical=False)).grid(row=0, column=5)
        # 右:分割結果
        right = Frame(main, bg="#e0e0e0", bd=2, relief=GROOVE)
        right.pack(side=LEFT, fill=Y)

        Label(right, text="分割結果プレビュー", bg="#e0e0e0").pack(pady=5)

        result_container = Frame(right, bg="#e0e0e0")
        result_container.pack(fill=BOTH, expand=True)

        self.result_canvas = Canvas(result_container, bg="#d0d0d0", width=200, bd=2, relief=SOLID)
        self.result_canvas.pack(side=LEFT, fill=BOTH, expand=True)

        result_scroll = Scrollbar(result_container, orient=VERTICAL, command=self.result_canvas.yview)
        result_scroll.pack(side=RIGHT, fill=Y)

        self.result_canvas.configure(yscrollcommand=result_scroll.set)
        self.result_inner = Frame(self.result_canvas, bg="#d0d0d0")
        self.result_canvas.create_window((0, 0), window=self.result_inner, anchor="nw")
        self.result_inner.bind("<Configure>", lambda e: self.result_canvas.configure(
            scrollregion=self.result_canvas.bbox("all")
        ))
    # ---------------- スクロール領域更新 ----------------
    def update_scrollregion(self, event=None):
        self.preview_canvas.configure(
            scrollregion=self.preview_canvas.bbox("all")
        )

    # ---------------- フォルダ選択 ----------------
    def select_folder(self):
        self.folder = filedialog.askdirectory()
        if self.folder:
            self.load_thumbnails()

    # ---------------- サムネイル読み込み ----------------
    def load_thumbnails(self):
        for w in self.thumb_inner.winfo_children():
            w.destroy()

        for file in sorted(os.listdir(self.folder)):
            if file.lower().endswith((".png", ".jpg", ".jpeg")):
                path = os.path.join(self.folder, file)
                try:
                    img = Image.open(path)
                    img.thumbnail((120, 120))
                    tk_img = ImageTk.PhotoImage(img)

                    btn = Button(self.thumb_inner, image=tk_img,
                                 command=lambda p=path: self.load_image_from_thumb(p),
                                 bg="#d0d0d0", relief=RIDGE, bd=2)
                    btn.image = tk_img
                    btn.pack(pady=4)
                except:
                    pass

    # ---------------- サムネイルクリックで画像読み込み ----------------
    def load_image_from_thumb(self, path):
        self.image_path = path
        self.load_image()
        self.load_split_data()
        self.update_zoomed_image()
        self.draw_image_and_lines()
        self.clear_result_thumbs()
        self.temp_results = []
        self.update_info_label()

    # ---------------- 画像読み込み ----------------
    def load_image(self):
        self.image = Image.open(self.image_path)
        self.update_zoomed_image()
        self.update_info_label()

    # ---------------- ズーム画像生成 ----------------
    def update_zoomed_image(self):
        if not self.image:
            return

        scale = self.zoom_percent / 100.0
        new_w = int(self.image.width * scale)
        new_h = int(self.image.height * scale)

        self.zoomed_image = self.image.resize((new_w, new_h), Image.LANCZOS)
        self.tk_image = ImageTk.PhotoImage(self.zoomed_image)

        self.canvas.config(width=new_w, height=new_h)
        self.preview_canvas.configure(scrollregion=(0, 0, new_w, new_h))

    # ---------------- 情報ラベル更新(1行) ----------------
    def update_info_label(self, click_x=None, click_y=None):
        if self.image:
            size_text = f"Size: {self.image.width}x{self.image.height}"
        else:
            size_text = "Size: - x -"

        if click_x is None or click_y is None:
            click_text = "Click: X=- Y=-"
        else:
            click_text = f"Click: X={click_x} Y={click_y}"

        zoom_text = f"Zoom: {self.zoom_percent}%"

        self.info_label.config(text=f"{size_text}    {click_text}    {zoom_text}")

    # ---------------- ズームスライダー変更 ----------------
    def on_zoom_slider(self, value):
        self.zoom_percent = int(float(value))
        self.update_zoomed_image()
        self.draw_image_and_lines()
        self.update_info_label()

    # ---------------- 線描画(ズーム対応) ----------------
    def draw_image_and_lines(self):
        if not self.zoomed_image:
            return

        self.canvas.delete("all")
        self.canvas.create_image(0, 0, anchor=NW, image=self.tk_image)

        scale = self.zoom_percent / 100.0
        w, h = self.zoomed_image.width, self.zoomed_image.height

        # ★ ズーム後の位置に変換して線を描画
        for x in self.split_data["vertical"]:
            zx = int(x * scale)
            self.canvas.create_line(zx, 0, zx, h, fill="red", width=2)

        for y in self.split_data["horizontal"]:
            zy = int(y * scale)
            self.canvas.create_line(0, zy, w, zy, fill="red", width=2)

    # ---------------- 左クリックで線追加(ズーム対応) ----------------
    def on_click(self, event):
        if not self.image:
            return

        scale = self.zoom_percent / 100.0
        orig_x = int(event.x / scale)
        orig_y = int(event.y / scale)

        self.update_info_label(orig_x, orig_y)

        mode = self.mode_var.get()

        if mode == "vertical":
            self.split_data["vertical"].append(orig_x)

        elif mode == "horizontal":
            self.split_data["horizontal"].append(orig_y)

        elif mode == "both":
            self.split_data["vertical"].append(orig_x)
            self.split_data["horizontal"].append(orig_y)

        self.draw_image_and_lines()

    # ---------------- 最後の線微調整 ----------------
    def adjust_last_line(self, delta, vertical=True):
        if vertical:
            if self.split_data["vertical"]:
                self.split_data["vertical"][-1] += delta
        else:
            if self.split_data["horizontal"]:
                self.split_data["horizontal"][-1] += delta

        self.draw_image_and_lines()

    # ---------------- 右ドラッグ開始 ----------------
    def on_drag_start(self, event):
        if not self.image:
            return

        scale = self.zoom_percent / 100.0
        x = int(event.x / scale)
        y = int(event.y / scale)
        threshold = 5

        for i, vx in enumerate(self.split_data["vertical"]):
            if abs(vx - x) <= threshold:
                self.dragging = True
                self.drag_line_type = "vertical"
                self.drag_index = i
                return

        for i, hy in enumerate(self.split_data["horizontal"]):
            if abs(hy - y) <= threshold:
                self.dragging = True
                self.drag_line_type = "horizontal"
                self.drag_index = i
                return

    # ---------------- 右ドラッグ中 ----------------
    def on_drag_move(self, event):
        if not self.dragging:
            return

        scale = self.zoom_percent / 100.0
        x = int(event.x / scale)
        y = int(event.y / scale)

        if self.drag_line_type == "vertical":
            self.split_data["vertical"][self.drag_index] = x
        else:
            self.split_data["horizontal"][self.drag_index] = y

        self.draw_image_and_lines()

    # ---------------- 右ドラッグ終了 ----------------
    def on_drag_end(self, event):
        self.dragging = False
        self.drag_line_type = None
        self.drag_index = None
    # ---------------- 最後の線削除 ----------------
    def undo_last(self):
        if self.mode_var.get() == "vertical" and self.split_data["vertical"]:
            self.split_data["vertical"].pop()
        elif self.mode_var.get() == "horizontal" and self.split_data["horizontal"]:
            self.split_data["horizontal"].pop()
        elif self.mode_var.get() == "both":
            if self.split_data["vertical"]:
                self.split_data["vertical"].pop()
            if self.split_data["horizontal"]:
                self.split_data["horizontal"].pop()

        self.draw_image_and_lines()

    # ---------------- 全線削除 ----------------
    def reset_lines(self):
        self.split_data = {"vertical": [], "horizontal": []}
        self.draw_image_and_lines()

    # ---------------- 分割ボタン ----------------
    def split_vertical(self):
        self.do_split(vertical=False, horizontal=True)

    def split_horizontal(self):
        self.do_split(vertical=True, horizontal=False)

    def split_both(self):
        self.do_split(vertical=True, horizontal=True)

    # ---------------- 分割処理(オフセット削除済み) ----------------
    def do_split(self, vertical, horizontal):
        if not self.image:
            return

        v = sorted(self.split_data["vertical"]) if vertical else []
        h = sorted(self.split_data["horizontal"]) if horizontal else []

        v = [0] + v + [self.image.width]
        h = [0] + h + [self.image.height]

        self.clear_result_thumbs()
        self.temp_results = []

        for i in range(len(v)-1):
            for j in range(len(h)-1):
                crop = self.image.crop((v[i], h[j], v[i+1], h[j+1]))
                self.temp_results.append(crop)
                self.add_result_thumb(crop)

    # ---------------- 分割結果プレビュー ----------------
    def clear_result_thumbs(self):
        for w in self.result_inner.winfo_children():
            w.destroy()

    def add_result_thumb(self, img):
        preview = img.copy()
        preview.thumbnail((140, 140))
        tk_img = ImageTk.PhotoImage(preview)

        lbl = Label(self.result_inner, image=tk_img, bg="#d0d0d0", relief=RIDGE, bd=2)
        lbl.image = tk_img
        lbl.pack(pady=4)

        lbl.bind("<Button-1>", lambda e, im=img: self.open_preview_window(im))

    # ---------------- 拡大表示 ----------------
    def open_preview_window(self, img):
        win = Toplevel(self.root)
        win.title("拡大プレビュー")

        tk_img = ImageTk.PhotoImage(img)
        lbl = Label(win, image=tk_img)
        lbl.image = tk_img
        lbl.pack(padx=10, pady=10)

        Button(win, text="閉じる", command=win.destroy).pack(pady=5)

    # ---------------- 分割結果保存 ----------------
    def save_split_results(self):
        if not self.temp_results:
            return

        base = os.path.splitext(os.path.basename(self.image_path))[0]
        out_dir = os.path.join(self.folder, "split_output")
        os.makedirs(out_dir, exist_ok=True)

        for idx, img in enumerate(self.temp_results):
            img.save(os.path.join(out_dir, f"{base}_{idx:04d}.png"))

    # ---------------- 線情報読み込み ----------------
    def load_split_data(self):
        program_name = os.path.splitext(os.path.basename(__file__))[0]
        json_path = program_name + ".json"

        if os.path.exists(json_path):
            with open(json_path, "r", encoding="utf-8") as f:
                self.split_data = json.load(f)
        else:
            self.split_data = {"vertical": [], "horizontal": []}

        if self.image:
            self.draw_image_and_lines()
            self.update_info_label()

    # ---------------- 線情報保存 ----------------
    def save_split_data(self):
        program_name = os.path.splitext(os.path.basename(__file__))[0]
        json_path = program_name + ".json"

        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(self.split_data, f, indent=2, ensure_ascii=False)

    # ---------------- バッチ処理(フォルダー内の全画像を自動分割) ----------------
    def batch_process(self):
        if not self.folder:
            return

        # 対象ファイル一覧
        files = [f for f in os.listdir(self.folder)
                 if f.lower().endswith((".png", ".jpg", ".jpeg"))]

        if not files:
            return

        out_dir = os.path.join(self.folder, "batch_output")
        os.makedirs(out_dir, exist_ok=True)

        for file in files:
            path = os.path.join(self.folder, file)
            img = Image.open(path)

            # 分割線が無い場合はスキップ
            if not self.split_data["vertical"] and not self.split_data["horizontal"]:
                continue

            v = sorted(self.split_data["vertical"])
            h = sorted(self.split_data["horizontal"])

            v = [0] + v + [img.width]
            h = [0] + h + [img.height]

            base = os.path.splitext(file)[0]

            # 分割して保存
            idx = 0
            for i in range(len(v)-1):
                for j in range(len(h)-1):
                    crop = img.crop((v[i], h[j], v[i+1], h[j+1]))
                    crop.save(os.path.join(out_dir, f"{base}_{idx:04d}.png"))
                    idx += 1

# ---------------- 起動 ----------------
if __name__ == "__main__":
    root = Tk()
    root.geometry("1200x800")
    app = ImageSplitterGUI(root)
    root.mainloop()

Python,画像

Posted by eightban