| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 图片拼接工具 - 针对GPT优化
- 将多张图片缩放后拼接,生成GPT可读取的最大尺寸图片
- """
- import os
- import sys
- import json
- from pathlib import Path
- from PIL import Image
- # GPT图片尺寸限制(根据实际API限制调整)
- # 为了减小base64编码后的体积,使用较小的尺寸
- # 2048x2048 对于黑白漫画来说足够清晰,AI可以识别文字和内容
- # 这样可以显著减小文件大小(约减少75%的体积)
- GPT_MAX_WIDTH = 2048
- GPT_MAX_HEIGHT = 2048
- GPT_MAX_PIXELS = GPT_MAX_WIDTH * GPT_MAX_HEIGHT # 约4.2MP
- # 为了保证文字清晰,单张图片的最小尺寸(针对黑白漫画优化)
- MIN_IMAGE_WIDTH = 600 # 最小宽度,保证文字可读(黑白漫画可以稍小)
- MIN_IMAGE_HEIGHT = 450 # 最小高度
- def calculate_grid_layout(images, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT):
- """
- 计算网格布局参数,最大化利用空间(确保单张网格图不超过限制)
-
- 参数:
- images: 图片列表
- max_width: 最大宽度
- max_height: 最大高度
-
- 返回:
- (cols, rows, scale_factor, single_width, single_height)
- """
- if not images:
- return 1, 1, 1.0, 0, 0
-
- # 获取单张图片的尺寸(假设所有图片尺寸相同)
- single_width = images[0].width
- single_height = images[0].height
-
- # 尝试不同的行列组合,找到能放最多图片且不超过限制的组合
- best_cols = 1
- best_rows = 1
- best_scale = 1.0
- max_images_per_grid = 0
-
- # 遍历可能的列数(从1开始,最多到能放下的最大列数)
- max_possible_cols = min(int(max_width / MIN_IMAGE_WIDTH), len(images))
-
- for cols in range(1, max_possible_cols + 1):
- # 计算需要的宽度
- needed_width = single_width * cols
-
- # 如果宽度超过限制,需要缩放
- if needed_width > max_width:
- scale_w = max_width / needed_width
- else:
- scale_w = 1.0
-
- # 计算缩放后单张图片的宽度
- scaled_single_width = single_width * scale_w
-
- # 确保缩放后宽度不小于最小值
- if scaled_single_width < MIN_IMAGE_WIDTH:
- continue
-
- # 计算缩放后单张图片的高度(保持宽高比)
- scaled_single_height = single_height * scale_w
-
- # 计算能放多少行
- max_rows = int(max_height / scaled_single_height)
- if max_rows < 1:
- continue
-
- # 计算这个布局能放多少张图片
- images_per_grid = cols * max_rows
-
- # 如果这个布局能放更多图片,记录它
- if images_per_grid > max_images_per_grid:
- max_images_per_grid = images_per_grid
- best_cols = cols
- best_rows = max_rows
- best_scale = scale_w
-
- # 如果没找到合适的,使用保守方案
- if max_images_per_grid == 0:
- # 计算最小缩放因子,确保单张图片能放下
- scale_w = min(max_width / single_width, max_height / single_height)
- # 确保缩放后不小于最小值
- if single_width * scale_w < MIN_IMAGE_WIDTH:
- scale_w = MIN_IMAGE_WIDTH / single_width
- if single_height * scale_w < MIN_IMAGE_HEIGHT:
- scale_w = MIN_IMAGE_HEIGHT / single_height
-
- best_scale = scale_w
- scaled_single_width = single_width * best_scale
- scaled_single_height = single_height * best_scale
- best_cols = max(1, int(max_width / scaled_single_width))
- best_rows = max(1, int(max_height / scaled_single_height))
- max_images_per_grid = best_cols * best_rows
-
- return best_cols, best_rows, best_scale, single_width, single_height
- def resize_image(img, scale_factor):
- """缩放图片"""
- # 如果缩放因子大于等于1.0,不放大(只缩小)
- if scale_factor >= 1.0:
- return img
-
- new_width = int(img.width * scale_factor)
- new_height = int(img.height * scale_factor)
-
- # 确保尺寸至少为1
- new_width = max(1, new_width)
- new_height = max(1, new_height)
-
- # 使用高质量缩放算法(LANCZOS)
- return img.resize((new_width, new_height), Image.Resampling.LANCZOS)
- def merge_images_for_gpt(image_dir, output_path, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT):
- """
- 将目录下的所有图片缩放后拼接,生成GPT可读取的图片
-
- 参数:
- image_dir: 图片目录(包含单页图片)
- output_path: 输出文件路径
- max_width: 单张图片最大宽度
- max_height: 单张图片最大高度
- """
- image_dir = Path(image_dir)
-
- if not image_dir.exists():
- print(f"❌ 错误:目录不存在 - {image_dir}")
- return False
-
- # 获取所有图片文件(按文件名排序)
- image_files = sorted(image_dir.glob("*.jpeg")) + sorted(image_dir.glob("*.jpg")) + \
- sorted(image_dir.glob("*.png"))
-
- # 过滤掉已拼接的图片
- image_files = [f for f in image_files if 'merged' not in f.name.lower() and 'part' not in f.name.lower()]
-
- if not image_files:
- print(f"❌ 在 {image_dir} 中未找到图片文件")
- return False
-
- print(f"📚 找到 {len(image_files)} 张图片")
- print(f"📁 图片目录: {image_dir}")
- print(f"📁 输出路径: {output_path}")
- print(f"🖼️ GPT最大尺寸: {max_width} x {max_height} 像素")
- print("-" * 60)
-
- # 加载所有图片
- print("📖 正在加载图片...")
- images = []
- for i, img_path in enumerate(image_files, 1):
- try:
- img = Image.open(img_path)
- # 转换为RGB模式(如果是灰度,保持灰度)
- if img.mode not in ('L', 'RGB'):
- img = img.convert('RGB')
- images.append(img)
- if i % 50 == 0:
- print(f" 已加载 {i}/{len(image_files)} 张...")
- except Exception as e:
- print(f" ⚠️ 跳过 {img_path.name}: {e}")
-
- if not images:
- print("❌ 没有成功加载的图片")
- return False
-
- print(f"✅ 成功加载 {len(images)} 张图片")
-
- # 检查所有图片尺寸是否一致
- first_width = images[0].width
- first_height = images[0].height
- all_same_size = all(img.width == first_width and img.height == first_height for img in images)
-
- if not all_same_size:
- print(f"⚠️ 警告:图片尺寸不一致,将使用第一张图片的尺寸作为基准")
- print(f" 第一张: {first_width}x{first_height}, 其他图片将被缩放")
-
- print(f"📊 单张图片尺寸: {first_width} x {first_height} 像素")
-
- # 计算最佳网格布局
- print("🔍 正在计算最佳网格布局(从右到左、从上到下,日式漫画阅读顺序)...")
- cols, rows, scale_factor, single_width, single_height = calculate_grid_layout(images, max_width, max_height)
-
- scaled_single_width = int(single_width * scale_factor)
- scaled_single_height = int(single_height * scale_factor)
- grid_width = scaled_single_width * cols
- grid_height = scaled_single_height * rows
-
- print(f"📐 布局方案: {cols}列 x {rows}行")
- print(f"📉 缩放比例: {scale_factor:.2%}")
- print(f"📊 单张图片缩放后: {scaled_single_width} x {scaled_single_height} 像素")
- print(f"📊 网格总尺寸: {grid_width} x {grid_height} 像素 ({grid_width * grid_height/1e6:.2f} MP)")
- print(f"📈 每张网格图可容纳: {cols * rows} 张原图")
-
- # 缩放所有图片
- print("🔄 正在缩放图片...")
- scaled_images = []
-
- for i, img in enumerate(images, 1):
- # 如果尺寸不一致,需要缩放
- if img.width != single_width or img.height != single_height:
- # 先缩放到标准尺寸
- img = img.resize((single_width, single_height), Image.Resampling.LANCZOS)
-
- # 应用缩放因子
- scaled_img = resize_image(img, scale_factor)
- scaled_images.append(scaled_img)
-
- if i % 50 == 0:
- print(f" 已缩放 {i}/{len(images)} 张...")
-
- # 创建输出子文件夹
- output_dir = Path(output_path).parent
- # 如果output_path已经在gpt_merged文件夹中,需要回到上一级
- if output_dir.name == "gpt_merged":
- output_dir = output_dir.parent
-
- output_folder_name = "gpt_merged"
- output_folder = output_dir / output_folder_name
- output_folder.mkdir(parents=True, exist_ok=True)
-
- output_stem = "gpt_merged"
- output_suffix = Path(output_path).suffix if Path(output_path).suffix else ".jpg"
-
- # 计算需要多少张网格图
- images_per_grid = cols * rows
- total_grids = (len(scaled_images) + images_per_grid - 1) // images_per_grid
-
- print(f"\n📦 需要生成 {total_grids} 张网格图片(每张包含最多 {images_per_grid} 张原图)")
-
- merged_files = []
-
- for grid_num in range(total_grids):
- start_idx = grid_num * images_per_grid
- end_idx = min(start_idx + images_per_grid, len(scaled_images))
- grid_images = scaled_images[start_idx:end_idx]
-
- # 计算当前网格的实际行数
- current_rows = (len(grid_images) + cols - 1) // cols
- current_grid_height = scaled_single_height * current_rows
-
- # 创建网格图(从右到左、从上到下排列,符合日式漫画阅读顺序)
- print(f"\n🔗 正在生成第 {grid_num + 1}/{total_grids} 张网格图({cols}列 x {current_rows}行,包含{len(grid_images)}张原图,从右到左排列)...")
- grid_image = Image.new('L' if grid_images[0].mode == 'L' else 'RGB', (grid_width, current_grid_height))
-
- for idx, img in enumerate(grid_images):
- row = idx // cols
- # 从右到左排列(日式漫画阅读顺序):第一张图片在最右边,然后向左排列
- col = (cols - 1) - (idx % cols)
- x = col * scaled_single_width
- y = row * scaled_single_height
- grid_image.paste(img, (x, y))
-
- if (idx + 1) % 50 == 0 or (idx + 1) == len(grid_images):
- print(f" 已拼接 {idx + 1}/{len(grid_images)} 张...")
-
- # 如果是最后一张图片且图片数量不足以填满整行,裁剪掉左侧空白
- is_last_grid = (grid_num == total_grids - 1)
- current_grid_width = grid_width
-
- if is_last_grid and len(grid_images) % cols != 0:
- # 由于是从右到左排列,空白在左侧
- # 计算最后一行的实际列数
- actual_cols_in_last_row = len(grid_images) % cols
- # 从右到左排列时,最后一行的图片在右侧,左侧是空白
- # 需要裁剪掉的左侧空白宽度 = (cols - actual_cols_in_last_row) * scaled_single_width
- left_blank_width = (cols - actual_cols_in_last_row) * scaled_single_width
-
- if left_blank_width > 0 and left_blank_width < grid_width:
- # 裁剪掉左侧空白
- actual_width = grid_width - left_blank_width
- cropped_image = grid_image.crop((left_blank_width, 0, grid_width, current_grid_height))
- grid_image = cropped_image
- current_grid_width = actual_width
- print(f" ✂️ 裁剪左侧空白: {left_blank_width}px,实际尺寸: {current_grid_width}x{current_grid_height}")
-
- # 保存
- if total_grids == 1:
- part_path = output_folder / f"{output_stem}{output_suffix}"
- else:
- part_path = output_folder / f"{output_stem}_part{grid_num + 1:02d}{output_suffix}"
-
- grid_image.save(part_path, 'JPEG', quality=85, optimize=True, progressive=True)
- file_size = part_path.stat().st_size / 1024 / 1024
- merged_files.append(part_path.name)
- print(f" ✅ 已保存: {part_path.name} ({current_grid_width}x{current_grid_height}, {file_size:.2f}MB)")
-
- print(f"\n✅ 拼接完成!共生成 {len(merged_files)} 张网格图片")
- return merged_files
- def main():
- """主函数"""
- # 获取项目根目录
- project_root = Path(__file__).parent.parent
-
- if len(sys.argv) > 1:
- # 如果提供了参数,使用指定的目录
- image_dir = Path(sys.argv[1])
- if len(sys.argv) > 2:
- output_path = Path(sys.argv[2])
- else:
- # 在图片目录下创建gpt_merged子文件夹
- output_folder = image_dir / "gpt_merged"
- output_folder.mkdir(parents=True, exist_ok=True)
- output_path = output_folder / "gpt_merged.jpg"
- else:
- # 默认使用漫画图片目录
- image_dir = project_root / "static" / "漫画" / "image" / "金-田-一-少-年-之-事-件-簿-日-文-版-第001卷"
- # 输出到子文件夹
- output_folder = image_dir / "gpt_merged"
- output_folder.mkdir(parents=True, exist_ok=True)
- output_path = output_folder / "gpt_merged.jpg"
-
- print("📖 图片拼接工具 - GPT优化版")
- print("=" * 60)
-
- result = merge_images_for_gpt(image_dir, output_path)
-
- if result:
- print("\n" + "=" * 60)
- print("✅ 处理完成!")
- if isinstance(result, list):
- print(f"📊 共生成 {len(result)} 张图片")
- for name in result:
- print(f" - {name}")
- else:
- print(f"📊 生成图片: {result}")
- else:
- print("\n❌ 处理失败")
- if __name__ == "__main__":
- main()
|