merge_images.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 图片拼接工具 - 针对GPT优化
  5. 将多张图片缩放后拼接,生成GPT可读取的最大尺寸图片
  6. """
  7. import os
  8. import sys
  9. import json
  10. from pathlib import Path
  11. from PIL import Image
  12. # GPT图片尺寸限制(根据实际API限制调整)
  13. # 为了减小base64编码后的体积,使用较小的尺寸
  14. # 2048x2048 对于黑白漫画来说足够清晰,AI可以识别文字和内容
  15. # 这样可以显著减小文件大小(约减少75%的体积)
  16. GPT_MAX_WIDTH = 2048
  17. GPT_MAX_HEIGHT = 2048
  18. GPT_MAX_PIXELS = GPT_MAX_WIDTH * GPT_MAX_HEIGHT # 约4.2MP
  19. # 为了保证文字清晰,单张图片的最小尺寸(针对黑白漫画优化)
  20. MIN_IMAGE_WIDTH = 600 # 最小宽度,保证文字可读(黑白漫画可以稍小)
  21. MIN_IMAGE_HEIGHT = 450 # 最小高度
  22. def calculate_grid_layout(images, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT):
  23. """
  24. 计算网格布局参数,最大化利用空间(确保单张网格图不超过限制)
  25. 参数:
  26. images: 图片列表
  27. max_width: 最大宽度
  28. max_height: 最大高度
  29. 返回:
  30. (cols, rows, scale_factor, single_width, single_height)
  31. """
  32. if not images:
  33. return 1, 1, 1.0, 0, 0
  34. # 获取单张图片的尺寸(假设所有图片尺寸相同)
  35. single_width = images[0].width
  36. single_height = images[0].height
  37. # 尝试不同的行列组合,找到能放最多图片且不超过限制的组合
  38. best_cols = 1
  39. best_rows = 1
  40. best_scale = 1.0
  41. max_images_per_grid = 0
  42. # 遍历可能的列数(从1开始,最多到能放下的最大列数)
  43. max_possible_cols = min(int(max_width / MIN_IMAGE_WIDTH), len(images))
  44. for cols in range(1, max_possible_cols + 1):
  45. # 计算需要的宽度
  46. needed_width = single_width * cols
  47. # 如果宽度超过限制,需要缩放
  48. if needed_width > max_width:
  49. scale_w = max_width / needed_width
  50. else:
  51. scale_w = 1.0
  52. # 计算缩放后单张图片的宽度
  53. scaled_single_width = single_width * scale_w
  54. # 确保缩放后宽度不小于最小值
  55. if scaled_single_width < MIN_IMAGE_WIDTH:
  56. continue
  57. # 计算缩放后单张图片的高度(保持宽高比)
  58. scaled_single_height = single_height * scale_w
  59. # 计算能放多少行
  60. max_rows = int(max_height / scaled_single_height)
  61. if max_rows < 1:
  62. continue
  63. # 计算这个布局能放多少张图片
  64. images_per_grid = cols * max_rows
  65. # 如果这个布局能放更多图片,记录它
  66. if images_per_grid > max_images_per_grid:
  67. max_images_per_grid = images_per_grid
  68. best_cols = cols
  69. best_rows = max_rows
  70. best_scale = scale_w
  71. # 如果没找到合适的,使用保守方案
  72. if max_images_per_grid == 0:
  73. # 计算最小缩放因子,确保单张图片能放下
  74. scale_w = min(max_width / single_width, max_height / single_height)
  75. # 确保缩放后不小于最小值
  76. if single_width * scale_w < MIN_IMAGE_WIDTH:
  77. scale_w = MIN_IMAGE_WIDTH / single_width
  78. if single_height * scale_w < MIN_IMAGE_HEIGHT:
  79. scale_w = MIN_IMAGE_HEIGHT / single_height
  80. best_scale = scale_w
  81. scaled_single_width = single_width * best_scale
  82. scaled_single_height = single_height * best_scale
  83. best_cols = max(1, int(max_width / scaled_single_width))
  84. best_rows = max(1, int(max_height / scaled_single_height))
  85. max_images_per_grid = best_cols * best_rows
  86. return best_cols, best_rows, best_scale, single_width, single_height
  87. def resize_image(img, scale_factor):
  88. """缩放图片"""
  89. # 如果缩放因子大于等于1.0,不放大(只缩小)
  90. if scale_factor >= 1.0:
  91. return img
  92. new_width = int(img.width * scale_factor)
  93. new_height = int(img.height * scale_factor)
  94. # 确保尺寸至少为1
  95. new_width = max(1, new_width)
  96. new_height = max(1, new_height)
  97. # 使用高质量缩放算法(LANCZOS)
  98. return img.resize((new_width, new_height), Image.Resampling.LANCZOS)
  99. def merge_images_for_gpt(image_dir, output_path, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT):
  100. """
  101. 将目录下的所有图片缩放后拼接,生成GPT可读取的图片
  102. 参数:
  103. image_dir: 图片目录(包含单页图片)
  104. output_path: 输出文件路径
  105. max_width: 单张图片最大宽度
  106. max_height: 单张图片最大高度
  107. """
  108. image_dir = Path(image_dir)
  109. if not image_dir.exists():
  110. print(f"❌ 错误:目录不存在 - {image_dir}")
  111. return False
  112. # 获取所有图片文件(按文件名排序)
  113. image_files = sorted(image_dir.glob("*.jpeg")) + sorted(image_dir.glob("*.jpg")) + \
  114. sorted(image_dir.glob("*.png"))
  115. # 过滤掉已拼接的图片
  116. image_files = [f for f in image_files if 'merged' not in f.name.lower() and 'part' not in f.name.lower()]
  117. if not image_files:
  118. print(f"❌ 在 {image_dir} 中未找到图片文件")
  119. return False
  120. print(f"📚 找到 {len(image_files)} 张图片")
  121. print(f"📁 图片目录: {image_dir}")
  122. print(f"📁 输出路径: {output_path}")
  123. print(f"🖼️ GPT最大尺寸: {max_width} x {max_height} 像素")
  124. print("-" * 60)
  125. # 加载所有图片
  126. print("📖 正在加载图片...")
  127. images = []
  128. for i, img_path in enumerate(image_files, 1):
  129. try:
  130. img = Image.open(img_path)
  131. # 转换为RGB模式(如果是灰度,保持灰度)
  132. if img.mode not in ('L', 'RGB'):
  133. img = img.convert('RGB')
  134. images.append(img)
  135. if i % 50 == 0:
  136. print(f" 已加载 {i}/{len(image_files)} 张...")
  137. except Exception as e:
  138. print(f" ⚠️ 跳过 {img_path.name}: {e}")
  139. if not images:
  140. print("❌ 没有成功加载的图片")
  141. return False
  142. print(f"✅ 成功加载 {len(images)} 张图片")
  143. # 检查所有图片尺寸是否一致
  144. first_width = images[0].width
  145. first_height = images[0].height
  146. all_same_size = all(img.width == first_width and img.height == first_height for img in images)
  147. if not all_same_size:
  148. print(f"⚠️ 警告:图片尺寸不一致,将使用第一张图片的尺寸作为基准")
  149. print(f" 第一张: {first_width}x{first_height}, 其他图片将被缩放")
  150. print(f"📊 单张图片尺寸: {first_width} x {first_height} 像素")
  151. # 计算最佳网格布局
  152. print("🔍 正在计算最佳网格布局(从右到左、从上到下,日式漫画阅读顺序)...")
  153. cols, rows, scale_factor, single_width, single_height = calculate_grid_layout(images, max_width, max_height)
  154. scaled_single_width = int(single_width * scale_factor)
  155. scaled_single_height = int(single_height * scale_factor)
  156. grid_width = scaled_single_width * cols
  157. grid_height = scaled_single_height * rows
  158. print(f"📐 布局方案: {cols}列 x {rows}行")
  159. print(f"📉 缩放比例: {scale_factor:.2%}")
  160. print(f"📊 单张图片缩放后: {scaled_single_width} x {scaled_single_height} 像素")
  161. print(f"📊 网格总尺寸: {grid_width} x {grid_height} 像素 ({grid_width * grid_height/1e6:.2f} MP)")
  162. print(f"📈 每张网格图可容纳: {cols * rows} 张原图")
  163. # 缩放所有图片
  164. print("🔄 正在缩放图片...")
  165. scaled_images = []
  166. for i, img in enumerate(images, 1):
  167. # 如果尺寸不一致,需要缩放
  168. if img.width != single_width or img.height != single_height:
  169. # 先缩放到标准尺寸
  170. img = img.resize((single_width, single_height), Image.Resampling.LANCZOS)
  171. # 应用缩放因子
  172. scaled_img = resize_image(img, scale_factor)
  173. scaled_images.append(scaled_img)
  174. if i % 50 == 0:
  175. print(f" 已缩放 {i}/{len(images)} 张...")
  176. # 创建输出子文件夹
  177. output_dir = Path(output_path).parent
  178. # 如果output_path已经在gpt_merged文件夹中,需要回到上一级
  179. if output_dir.name == "gpt_merged":
  180. output_dir = output_dir.parent
  181. output_folder_name = "gpt_merged"
  182. output_folder = output_dir / output_folder_name
  183. output_folder.mkdir(parents=True, exist_ok=True)
  184. output_stem = "gpt_merged"
  185. output_suffix = Path(output_path).suffix if Path(output_path).suffix else ".jpg"
  186. # 计算需要多少张网格图
  187. images_per_grid = cols * rows
  188. total_grids = (len(scaled_images) + images_per_grid - 1) // images_per_grid
  189. print(f"\n📦 需要生成 {total_grids} 张网格图片(每张包含最多 {images_per_grid} 张原图)")
  190. merged_files = []
  191. for grid_num in range(total_grids):
  192. start_idx = grid_num * images_per_grid
  193. end_idx = min(start_idx + images_per_grid, len(scaled_images))
  194. grid_images = scaled_images[start_idx:end_idx]
  195. # 计算当前网格的实际行数
  196. current_rows = (len(grid_images) + cols - 1) // cols
  197. current_grid_height = scaled_single_height * current_rows
  198. # 创建网格图(从右到左、从上到下排列,符合日式漫画阅读顺序)
  199. print(f"\n🔗 正在生成第 {grid_num + 1}/{total_grids} 张网格图({cols}列 x {current_rows}行,包含{len(grid_images)}张原图,从右到左排列)...")
  200. grid_image = Image.new('L' if grid_images[0].mode == 'L' else 'RGB', (grid_width, current_grid_height))
  201. for idx, img in enumerate(grid_images):
  202. row = idx // cols
  203. # 从右到左排列(日式漫画阅读顺序):第一张图片在最右边,然后向左排列
  204. col = (cols - 1) - (idx % cols)
  205. x = col * scaled_single_width
  206. y = row * scaled_single_height
  207. grid_image.paste(img, (x, y))
  208. if (idx + 1) % 50 == 0 or (idx + 1) == len(grid_images):
  209. print(f" 已拼接 {idx + 1}/{len(grid_images)} 张...")
  210. # 如果是最后一张图片且图片数量不足以填满整行,裁剪掉左侧空白
  211. is_last_grid = (grid_num == total_grids - 1)
  212. current_grid_width = grid_width
  213. if is_last_grid and len(grid_images) % cols != 0:
  214. # 由于是从右到左排列,空白在左侧
  215. # 计算最后一行的实际列数
  216. actual_cols_in_last_row = len(grid_images) % cols
  217. # 从右到左排列时,最后一行的图片在右侧,左侧是空白
  218. # 需要裁剪掉的左侧空白宽度 = (cols - actual_cols_in_last_row) * scaled_single_width
  219. left_blank_width = (cols - actual_cols_in_last_row) * scaled_single_width
  220. if left_blank_width > 0 and left_blank_width < grid_width:
  221. # 裁剪掉左侧空白
  222. actual_width = grid_width - left_blank_width
  223. cropped_image = grid_image.crop((left_blank_width, 0, grid_width, current_grid_height))
  224. grid_image = cropped_image
  225. current_grid_width = actual_width
  226. print(f" ✂️ 裁剪左侧空白: {left_blank_width}px,实际尺寸: {current_grid_width}x{current_grid_height}")
  227. # 保存
  228. if total_grids == 1:
  229. part_path = output_folder / f"{output_stem}{output_suffix}"
  230. else:
  231. part_path = output_folder / f"{output_stem}_part{grid_num + 1:02d}{output_suffix}"
  232. grid_image.save(part_path, 'JPEG', quality=85, optimize=True, progressive=True)
  233. file_size = part_path.stat().st_size / 1024 / 1024
  234. merged_files.append(part_path.name)
  235. print(f" ✅ 已保存: {part_path.name} ({current_grid_width}x{current_grid_height}, {file_size:.2f}MB)")
  236. print(f"\n✅ 拼接完成!共生成 {len(merged_files)} 张网格图片")
  237. return merged_files
  238. def main():
  239. """主函数"""
  240. # 获取项目根目录
  241. project_root = Path(__file__).parent.parent
  242. if len(sys.argv) > 1:
  243. # 如果提供了参数,使用指定的目录
  244. image_dir = Path(sys.argv[1])
  245. if len(sys.argv) > 2:
  246. output_path = Path(sys.argv[2])
  247. else:
  248. # 在图片目录下创建gpt_merged子文件夹
  249. output_folder = image_dir / "gpt_merged"
  250. output_folder.mkdir(parents=True, exist_ok=True)
  251. output_path = output_folder / "gpt_merged.jpg"
  252. else:
  253. # 默认使用漫画图片目录
  254. image_dir = project_root / "static" / "漫画" / "image" / "金-田-一-少-年-之-事-件-簿-日-文-版-第001卷"
  255. # 输出到子文件夹
  256. output_folder = image_dir / "gpt_merged"
  257. output_folder.mkdir(parents=True, exist_ok=True)
  258. output_path = output_folder / "gpt_merged.jpg"
  259. print("📖 图片拼接工具 - GPT优化版")
  260. print("=" * 60)
  261. result = merge_images_for_gpt(image_dir, output_path)
  262. if result:
  263. print("\n" + "=" * 60)
  264. print("✅ 处理完成!")
  265. if isinstance(result, list):
  266. print(f"📊 共生成 {len(result)} 张图片")
  267. for name in result:
  268. print(f" - {name}")
  269. else:
  270. print(f"📊 生成图片: {result}")
  271. else:
  272. print("\n❌ 处理失败")
  273. if __name__ == "__main__":
  274. main()