プロンプト維持してリサイズ、画像連結するスクリプト
としあき謹製
png_merge_with_prompts.py
というファイル名で保存
必要なら pip install pillow
実行は input_pngs
フォルダにpngを入れてから python png_merge_with_prompts.py
プロンプト維持してリサイズ、画像連結されて merged_small.png
に保存される
原寸は merged.png
、ログは png_merge_with_prompts.log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | import sys
import math
import datetime
import argparse
from typing import List, Tuple, Optional
from pathlib import Path
from PIL import Image, PngImagePlugin
def main() -> None:
args = parse_args()
log_message(f"main: 開始 input={args.input}, output={args.output}")
input_folder = Path(args.input)
validate_input_folder(input_folder)
image_paths = get_image_paths(input_folder)
prompts = collect_prompts(image_paths)
try:
process_images(image_paths, args.output, prompts)
except (OSError, IOError, ValueError, RuntimeError) as e:
handle_error(e)
def log_message(message: str) -> None:
LOG_FILE = Path(__file__).with_suffix('.log').name
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] {message}\n")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="画像を連結し、プロンプトメタデータを埋め込む")
parser.add_argument("--input", type=str, default="input_pngs", help="入力画像フォルダ")
parser.add_argument("--output", type=str, default="merged.png", help="出力画像ファイル名")
return parser.parse_args()
def validate_input_folder(input_folder: Path) -> None:
if not input_folder.exists():
log_message(f"Error: 入力フォルダ {input_folder} が存在しません。")
print(f"Error: 入力フォルダ {input_folder} が存在しません。", file=sys.stderr)
sys.exit(1)
def get_image_paths(input_folder: Path) -> List[Path]:
image_paths = sorted(list(input_folder.glob("*.png")))
if len(image_paths) == 0:
log_message(f"Error: フォルダ {input_folder} に画像が見つかりません。")
print(f"Error: フォルダ {input_folder} に画像が見つかりません。", file=sys.stderr)
sys.exit(1)
log_message(f"{len(image_paths)}枚の画像が見つかりました。")
return image_paths
def save_image_with_metadata(image: Image.Image, output_path: Path, prompts: List[str]) -> None:
"""画像を保存し、メタデータを埋め込む"""
combined_prompt = format_prompts(prompts)
meta = PngImagePlugin.PngInfo()
meta.add_text("parameters", combined_prompt)
try:
print(f"画像ファイルに書き込み中... ({output_path.name})")
image.save(output_path, pnginfo=meta)
log_message(f"画像保存完了(メタデータ付き): {output_path}")
except (OSError, IOError, ValueError) as e:
log_message(f"画像保存失敗: {output_path} ({e})")
raise RuntimeError(f"画像保存失敗: {output_path} ({e})") from e
def process_images(image_paths: List[Path], output: str, prompts: List[str]) -> None:
"""画像処理のメインフロー"""
# 画像を連結
grid_size = determine_grid_size(len(image_paths))
combined = combine_images(image_paths, grid_size)
output_path = Path(output).absolute()
# 原寸版を保存
save_image_with_metadata(combined, output_path, prompts)
# 縮小版を作成・保存(連結前の画像サイズに合わせる)
resized_combined = create_resized_to_original_size(image_paths, combined)
resized_output_path = output_path.with_stem(output_path.stem + "_small")
save_image_with_metadata(resized_combined, resized_output_path, prompts)
# リソース解放
combined.close()
resized_combined.close()
log_message(f"完了: 原寸版 {output_path}, 縮小版 {resized_output_path} に{len(image_paths)}つのプロンプトを埋め込み済み")
def handle_error(e: Exception) -> None:
log_message(f"エラー: {e}")
print(f"エラー: {e}", file=sys.stderr)
sys.exit(1)
def extract_prompt_metadata(png_file: Path) -> Optional[str]:
"""
Pillowでparametersテキストチャンクを抽出
"""
try:
with Image.open(png_file) as img:
return img.info.get('parameters')
except (OSError, IOError) as e:
log_message(f"メタデータ取得失敗: {png_file} ({e})")
print(f"メタデータ取得失敗: {png_file} ({e})", file=sys.stderr)
return None
def get_image_size(img: Image.Image) -> Tuple[int, int]:
"""画像サイズ取得"""
size = img.size
log_message(f"画像サイズ取得: {size}")
return size
def get_max_image_size(images: List[Image.Image]) -> Tuple[int, int]:
"""画像リストから最大サイズを取得"""
widths, heights = zip(*(get_image_size(img) for img in images))
max_size = (max(widths), max(heights))
log_message(f"最大画像サイズ: {max_size}")
return max_size
def calc_grid_position(index: int, grid_cols: int, max_width: int, max_height: int) -> Tuple[int, int]:
"""グリッド座標計算"""
x = (index % grid_cols) * max_width
y = (index // grid_cols) * max_height
log_message(f"グリッド座標計算: index={index}, x={x}, y={y}")
return x, y
def determine_grid_size(image_count: int) -> Tuple[int, int]:
"""
画像枚数に応じて最適なグリッドサイズを決定
正方形に近い形になるよう計算
"""
if image_count <= 0:
return (1, 1)
# 平方根を取って、正方形に近いグリッドサイズを計算
sqrt_count = math.sqrt(image_count)
# 列数は平方根を切り上げ
cols = math.ceil(sqrt_count)
# 行数は画像枚数を列数で割った値を切り上げ
rows = math.ceil(image_count / cols)
grid_size = (cols, rows)
log_message(f"グリッドサイズ決定: {image_count}枚の画像に対して{grid_size}")
return grid_size
def format_prompts(prompts: List[str]) -> str:
"""プロンプトリストを整形"""
formatted_parts = []
for i, p in enumerate(prompts):
if i == len(prompts) - 1: # 最後の画像
formatted_parts.append(f"--- Image {i+1} ---\n{p}\n--- End of Images ---")
else:
formatted_parts.append(f"--- Image {i+1} ---\n{p}")
formatted = "\n".join(formatted_parts)
log_message(f"プロンプト整形: {formatted}")
return formatted
def load_images(image_paths: List[Path]) -> List[Image.Image]:
"""画像ファイルを開いてリストで返す"""
log_message(f"画像読み込み開始: {image_paths}")
images = []
for path in image_paths:
try:
img = Image.open(path)
images.append(img)
log_message(f"画像読み込み成功: {path}")
except (OSError, IOError) as e:
log_message(f"画像読み込み失敗: {path} ({e})")
print(f"画像読み込み失敗: {path} ({e})", file=sys.stderr)
raise RuntimeError(f"画像読み込み失敗: {path} ({e})") from e
if not images:
log_message("画像がありません(image_pathsが空です)")
raise ValueError("画像がありません(image_pathsが空です)")
return images
def create_resized_image(image: Image.Image, scale: float) -> Image.Image:
"""画像を指定倍率でリサイズ"""
original_size = image.size
new_size = (int(original_size[0] * scale), int(original_size[1] * scale))
resized = image.resize(new_size, Image.Resampling.LANCZOS)
log_message(f"画像リサイズ: {original_size} -> {new_size} (倍率: {scale})")
return resized
def create_resized_to_original_size(image_paths: List[Path], combined_image: Image.Image) -> Image.Image:
"""連結画像を短い辺が元画像の短い辺になるようにリサイズ"""
# 最初の画像のサイズを基準とする
with Image.open(image_paths[0]) as first_img:
original_width, original_height = first_img.size
combined_width, combined_height = combined_image.size
# 元画像の短い辺を取得
original_short_side = min(original_width, original_height)
# 連結画像の短い辺を取得
combined_short_side = min(combined_width, combined_height)
# 短い辺を元画像の短い辺に合わせるスケールを計算
scale = original_short_side / combined_short_side
target_width = int(combined_width * scale)
target_height = int(combined_height * scale)
resized = combined_image.resize((target_width, target_height), Image.Resampling.LANCZOS)
log_message(f"連結画像を短い辺基準でリサイズ: {combined_image.size} -> {target_width}x{target_height} (スケール: {scale:.4f}, 元画像短い辺: {original_short_side})")
return resized
def combine_images(image_paths: List[Path], grid_size: Tuple[int, int]) -> Image.Image:
"""
画像をグリッド状に連結
"""
log_message(f"combine_images: 開始 {image_paths}, grid_size={grid_size}")
images = load_images(image_paths)
max_width, max_height = get_max_image_size(images)
grid_cols, grid_rows = grid_size
if len(set(img.size[0] for img in images)) > 1 or len(set(img.size[1] for img in images)) > 1:
log_message("警告: 画像サイズが一致していません。最大サイズで連結します。")
print("警告: 画像サイズが一致していません。最大サイズで連結します。", file=sys.stderr)
combined_image = Image.new('RGB', (max_width * grid_cols, max_height * grid_rows))
for index, img in enumerate(images):
x, y = calc_grid_position(index, grid_cols, max_width, max_height)
combined_image.paste(img, (x, y))
log_message(f"画像貼り付け: index={index}, x={x}, y={y}")
for img in images:
img.close()
log_message("combine_images: 完了")
return combined_image
def collect_prompts(image_paths: List[Path]) -> List[str]:
"""画像ごとのプロンプトを抽出"""
log_message(f"collect_prompts: 開始 {image_paths}")
prompts = []
for path in image_paths:
prompt = extract_prompt_metadata(path)
prompts.append(prompt or "[No parameters]")
log_message(f"プロンプト抽出: {path} -> {prompt}")
log_message(f"collect_prompts: 完了 {prompts}")
return prompts
if __name__ == "__main__":
main()
|