0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python コードで画像を一括トリミングするコードを作って GUI アプリにしてみた。

Last updated at Posted at 2025-12-17

スマホのスクリーンショットなどの画像を一括でトリミングしたいときってありませんか?
今回は指定した領域でトリミングするツールを作ってみました。
コピペでも動きますので、よろしければ使ってみてください。

また Github にプロジェクトを公開しています。
よろしければご利用ください。

環境

今回の環境は下記のとおりです。

開発端末

  • OS: Windows 11 Pro
  • CPU: Intel Core i7-13620H
  • メモリ: 32GB
  • GPU: NVIDIA GeForce RTX 4060 Laptop

Python 環境

  • Python: 3.11.9
  • ライブラリ
    • pillow
    • tkinterdnd2
    • pyinstaller

コードの紹介

基本的なコードは以前作ったこちらのツールから流用しています。

画像加工はお馴染みの Pillow を使います。

ライブラリのインストール

ライブラリのインストールはお使いの環境にあわせて行ってください。

pip の場合

pip install pillow
pip install tkinterdnd2
pip install pyinstaller

poetry の場合

事前に pyproject.toml の Python バージョンを修正する必要があります。

[tool.poetry.dependencies]
- python = "^=3.11"
+ python = ">=3.11,<3.13"

修正を行い、上書き保存したら、モジュールのインストールを行います。

poetry add pillow
poetry add tkinterdnd2
poetry add pyinstaller

Poetry について
Poetry の詳細は、下記の記事を参考にしてください。
https://qiita.com/IoriGunji/items/290db948c11fdc81046a

ソースコード

Python のソースコードは下記のとおりです。

image_cropper.py
import os
import re
import glob
import sys
from typing import Final, Tuple
from PIL import Image

IMAGE_EXTS: Final[Tuple[str, ...]] = ['png', 'jpg', 'jpeg', 'bmp']
CROP_BOX: Final[Tuple[int, ...]] = [100, 100, 500, 500] #左, 上, 右, 下 

def main():
    chdir = os.path.dirname(__file__)
    files = glob.glob(os.path.join(chdir, '*'))
    for file in files:
        for ext in IMAGE_EXTS:
            if re.search(f'\.{ext}$', file):
                image_crop(file, CROP_BOX)

def image_crop(img_path: str, crop_box: Tuple[int, ...]):
    # 画像読み込み
    try:
        img = Image.open(img_path)
        print(f'Loaded: {img_path}')
    except FileNotFoundError:
        print(f'Error: {img_path} not found. Please check the file name and path!')
        sys.exit()
    # 各種パスの取得
    dir, file = os.path.split(img_path)
    # 出力先ディレクトリの作成
    output = 'cropped'
    os.makedirs(os.path.join(dir, output), exist_ok=True)
    # 画像サイズの取得
    img_width, img_height = img.size
    # バリデーションチェック
    left, upper, right, lower = crop_box
    # 1. 座標の論理チェック
    if left >= right or upper >= lower:
        print(f'Error: The crop area coordinates are invalid!')
        print(f'Specified area: left={left}, upper={upper}, right={right}, lower={lower}')
        print('Area Conditions: left < right and upper < lower must be true.')
        sys.exit()
    # 2. 範囲外のチェック
    if left < 0 or upper < 0 or right > img_width or lower > img_height:
        print(f'Warning: The cropping area exceeds the image range.\nCrop to fit the image range.')
        # 座標を画像の範囲内に調整
        adjusted_left = max(0, left)
        adjusted_upper = max(0, upper)
        adjusted_right = min(img_width, right)
        adjusted_lower = min(img_height, lower)
        # 調整後に有効な切り抜き領域が残っているかチェック
        if adjusted_left >= adjusted_right or adjusted_upper >= adjusted_lower:
            print('Error: The specified crop area is completely outside the bounds of the image, so there is no valid area!')
            sys.exit()
        # 調整後のボックスで上書き
        crop_box = (adjusted_left, adjusted_upper, adjusted_right, adjusted_lower)
    # 画像のトリミング
    cropped_img = img.crop(crop_box)
    # トリミング後の画像の保存
    cropped_img.save(os.path.join(dir, output, file), quality = 100)
    print('Done: The image has been cropped.')

if __name__ == "__main__":
    main()
gui.py
import sys
import re
import configparser
import tkinter as tk
import tkinter.messagebox as mb
from tkinterdnd2 import DND_FILES, TkinterDnD
from imagecropper import image_cropper

def main():
    config = read_config()
    gui(config)

# GUI ウィンドウ
def gui(config):
    # ウィンドウ設定
    root = TkinterDnD.Tk()
    root.title('Image Cropper')
    root.geometry('330x150')
    default_color = root.cget("bg")
    # 変数宣言
    left = tk.StringVar()
    left.set(config['left'])
    upper = tk.StringVar()
    upper.set(config['upper'])
    right = tk.StringVar()
    right.set(config['right'])
    lower = tk.StringVar()
    lower.set(config['lower'])
    # ボーダー設定
    border_frame = tk.Frame(root, height=130, width=150, pady=10, padx=10, relief=tk.SOLID, bg=default_color, bd=1)
    border_frame.place(x=10, y=10)
    # 左
    border_left_label = tk.Label(border_frame, text='Left:')
    border_left_label.place(x=0, y=10)
    border_left_input = tk.Entry(border_frame, textvariable=left, readonlybackground='white', justify=tk.CENTER)
    border_left_input.configure(validate='key', vcmd=(border_left_input.register(pre_validation_crop), '%s', '%P'))
    border_left_input.place(x=50, y=10, width=60, height=21)
    border_left_unit = tk.Label(border_frame, text='px')
    border_left_unit.place(x=110, y=10)
    # 上
    border_upper_label = tk.Label(border_frame, text='Upper:')
    border_upper_label.place(x=0, y=35)
    border_upper_input = tk.Entry(border_frame, textvariable=upper, readonlybackground='white', justify=tk.CENTER)
    border_upper_input.configure(validate='key', vcmd=(border_upper_input.register(pre_validation_crop), '%s', '%P'))
    border_upper_input.place(x=50, y=35, width=60, height=21)
    border_upper_unit = tk.Label(border_frame, text='px')
    border_upper_unit.place(x=110, y=35)
    # 右
    border_right_label = tk.Label(border_frame, text='Right')
    border_right_label.place(x=0, y=60)
    border_right_input = tk.Entry(border_frame, textvariable=right, readonlybackground='white', justify=tk.CENTER)
    border_right_input.configure(validate='key', vcmd=(border_right_input.register(pre_validation_crop), '%s', '%P'))
    border_right_input.place(x=50, y=60, width=60, height=21)
    border_right_unit = tk.Label(border_frame, text='px')
    border_right_unit.place(x=110, y=60)
    # 下
    border_lower_label = tk.Label(border_frame, text='Lower')
    border_lower_label.place(x=0, y=85)
    border_lower_input = tk.Entry(border_frame, textvariable=lower, readonlybackground='white', justify=tk.CENTER)
    border_lower_input.configure(validate='key', vcmd=(border_lower_input.register(pre_validation_crop), '%s', '%P'))
    border_lower_input.place(x=50, y=85, width=60, height=21)
    border_lower_unit = tk.Label(border_frame, text='px')
    border_lower_unit.place(x=110, y=85)
    # ドロップエリア
    drop_frame = tk.Frame(root, height=130, width=150, pady=10, padx=10, relief=tk.SOLID, bg=default_color, bd=1)
    drop_frame.drop_target_register(DND_FILES)
    drop_frame.dnd_bind('<<Drop>>', lambda e: drop(e.data, left.get(), upper.get(), right.get(), lower.get()))
    drop_frame.place(x=170, y=10)
    # ドロップテキストラベル
    drop_label = tk.Label(drop_frame, text='* Drop image file here')
    drop_label.place(x=65, y=55, anchor=tk.CENTER)
    # 各種タイトル
    border_label = tk.Label(root, text='Crop settings')
    border_label.place(x=20, y=0)
    dorp_label = tk.Label(root, text='Drop files')
    dorp_label.place(x=180, y=0)
    # 閉じるボタンの制御
    root.protocol("WM_DELETE_WINDOW", lambda: exit(left.get(), upper.get(), right.get(), lower.get()))
    # GUI描画
    root.mainloop()

# 画像処理
def drop(files, left, upper, right, lower):
    left = int(left or 0)
    upper = int(upper or 0)
    right = int(right or sys.maxsize)
    lower = int(lower or sys.maxsize)
    files = dnd2_parse_files(files)
    for file in files:
        print(file)
        for ext in image_cropper.IMAGE_EXTS:
            if re.search(f'\.{ext}$', file):
                image_cropper.image_crop(file, [left, upper, right, lower])

# DnD2 ファイルパスの解析
def dnd2_parse_files(files_str):
    start = 0
    length = len(files_str)
    files = []
    while start < length:
        if files_str[start] == '{':
            end = files_str.find('}', start+1)
            file = files_str[start+1 : end]
            start = end + 2
        else:
            end = files_str.find(' ', start)
            if end < 0:
                file = files_str[start:]
                start = length
            else:
                file = files_str[start : end]
                start = end + 1
        files.append(file)
    return files

# 終了処理
def exit(left, upper, right, lower):
    save_config(left, upper, right, lower)
    sys.exit()

# バリデーションチェック
def pre_validation_crop(before_word, after_word):
    return ((after_word.isdecimal() or after_word == '') and (len(after_word) <= 8 or len(after_word) == 0))

# 設定の読み込み
def read_config():
    config = configparser.ConfigParser()
    config.read('config.ini', encoding='utf-8')
    default = config['DEFAULT']
    configs = {
        'left': default.get('left') if default.get('left') != None else ''
        , 'upper': default.get('upper') if default.get('upper') != None else ''
        , 'right': default.get('right') if default.get('right') != None else ''
        , 'lower': default.get('lower') if default.get('lower') != None else ''

    }
    return configs

# 設定の保存
def save_config(left, upper, right, lower):
    config = configparser.ConfigParser()
    config.set('DEFAULT', 'left', left)
    config.set('DEFAULT', 'upper', upper)
    config.set('DEFAULT', 'right', right)
    config.set('DEFAULT', 'lower', lower)
    with open('config.ini', 'w', encoding='utf-8') as configfile:
        config.write(configfile)

if __name__ == "__main__":
    main()

ディレクトリ構成

ディレクトリ構成は下記のようになっています。

└ プロジェクトディレクトリ
 └ imagecropper
  ├ gui.py
  └ image_cropper.py

使い方

  1. gui.py を実行すると下記のような GUI が表示されます。
    rapture_20250813145637.png
  2. トリミング領域の設定を行います。
    • Left にトリミングしたい領域の左側の座標を指定します。(例:100
    • Upper にトリミングしたい領域の上側の座標を指定します。(例:200
    • Right にトリミングしたい領域の右側の座標を指定します。(例:500
    • Lower にトリミングしたい領域の下側の座標を指定します。(例:600
  3. Drop files の枠線内にトリミングしたい画像をドラッグ&ドロップします。
  4. 変換された画像は、元画像と同一ディレクトリ内の ./cropped フォルダに出力されます。

PyInstaller

PyInstaller で exe ファイルにビルドする方法を紹介します。

下記の前回の記事と同様の内容になります。

hook-tkinterdnd2.py のダウンロード

今回はモジュールに TkinterDnD2 を利用しているため PyInstaller を利用する場合は、 hook-tkinterdnd2.py が必要になります。
Github からダウンロードして利用してください。

hook-tkinterdnd2.pygui.pyimage_cropper.py のスクリプトと同一のディレクトリに保存します。

ディレクトリ構成

ディレクトリ構成は下記のようになっています。

└ プロジェクトディレクトリ
 └ imagecropper
  ├ gui.py
  ├ hook-tkinterdnd2.py
  ├ icon.ico(※任意)
  └ image_cropper.py

PyInstaller の実行

PyInstaller で実行ファイルを作成する場合は、次のオプションを追加します。

--additional-hooks-dir [hook-tkinterdnd2.py が保存されているディレクトリのパス]

pip の場合

pyinstaller ./imagecropper/gui.py --name ImageCropper --onefile --noconsole --collect-data tkinterdnd2 --additional-hooks-dir ./imagecropper

Poetry の場合

poetry run pyinstaller ./imagecropper/gui.py --name ImageCropper --onefile --noconsole --collect-data tkinterdnd2 --additional-hooks-dir ./imagecropper

アイコンも含める場合

poetry run pyinstaller ./imagecropper/gui.py --name ImageCropper --onefile --noconsole --icon=./imagecropper/icon.ico --collect-data tkinterdnd2 --additional-hooks-dir ./imagecropper
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?