スマホのスクリーンショットなどの画像を一括でトリミングしたいときってありませんか?
今回は指定した領域でトリミングするツールを作ってみました。
コピペでも動きますので、よろしければ使ってみてください。
また 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 のソースコードは下記のとおりです。
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()
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
使い方
-
gui.pyを実行すると下記のような GUI が表示されます。
- トリミング領域の設定を行います。
-
Leftにトリミングしたい領域の左側の座標を指定します。(例:100) -
Upperにトリミングしたい領域の上側の座標を指定します。(例:200) -
Rightにトリミングしたい領域の右側の座標を指定します。(例:500) -
Lowerにトリミングしたい領域の下側の座標を指定します。(例:600)
-
-
Drop filesの枠線内にトリミングしたい画像をドラッグ&ドロップします。 - 変換された画像は、元画像と同一ディレクトリ内の
./croppedフォルダに出力されます。
PyInstaller
PyInstaller で exe ファイルにビルドする方法を紹介します。
下記の前回の記事と同様の内容になります。
hook-tkinterdnd2.py のダウンロード
今回はモジュールに TkinterDnD2 を利用しているため PyInstaller を利用する場合は、 hook-tkinterdnd2.py が必要になります。
Github からダウンロードして利用してください。
hook-tkinterdnd2.py を gui.py や image_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