Initial commit to git.yoin
This commit is contained in:
251
image-service/scripts/merge_long_image.py
Normal file
251
image-service/scripts/merge_long_image.py
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
长图拼接脚本 (Merge Long Image)
|
||||
将多张图片按顺序垂直拼接成一张微信长图
|
||||
|
||||
Author: 翟星人
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import glob as glob_module
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
|
||||
class LongImageMerger:
|
||||
"""长图拼接器"""
|
||||
|
||||
def __init__(self, target_width: int = 1080):
|
||||
"""
|
||||
初始化拼接器
|
||||
|
||||
Args:
|
||||
target_width: 目标宽度,默认1080(微信推荐宽度)
|
||||
"""
|
||||
self.target_width = target_width
|
||||
|
||||
def _blend_images(self, img_top: Image.Image, img_bottom: Image.Image, blend_height: int) -> Image.Image:
|
||||
"""
|
||||
在两张图的接缝处创建渐变融合过渡
|
||||
|
||||
Args:
|
||||
img_top: 上方图片
|
||||
img_bottom: 下方图片
|
||||
blend_height: 融合区域高度(像素)
|
||||
|
||||
Returns:
|
||||
融合后的下方图片(顶部已与上方图片底部融合)
|
||||
"""
|
||||
blend_height = min(blend_height, img_top.height // 4, img_bottom.height // 4)
|
||||
|
||||
top_region = img_top.crop((0, img_top.height - blend_height, img_top.width, img_top.height))
|
||||
bottom_region = img_bottom.crop((0, 0, img_bottom.width, blend_height))
|
||||
|
||||
top_array = np.array(top_region, dtype=np.float32)
|
||||
bottom_array = np.array(bottom_region, dtype=np.float32)
|
||||
|
||||
alpha = np.linspace(1, 0, blend_height).reshape(-1, 1, 1)
|
||||
|
||||
blended_array = top_array * alpha + bottom_array * (1 - alpha)
|
||||
blended_array = np.clip(blended_array, 0, 255).astype(np.uint8)
|
||||
|
||||
blended_region = Image.fromarray(blended_array)
|
||||
|
||||
result = img_bottom.copy()
|
||||
result.paste(blended_region, (0, 0))
|
||||
|
||||
return result
|
||||
|
||||
def merge(
|
||||
self,
|
||||
image_paths: List[str],
|
||||
output_path: str,
|
||||
gap: int = 0,
|
||||
background_color: str = "white",
|
||||
blend: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
拼接多张图片为长图
|
||||
|
||||
Args:
|
||||
image_paths: 图片路径列表,按顺序拼接
|
||||
output_path: 输出文件路径
|
||||
gap: 图片之间的间隔像素,默认0
|
||||
background_color: 背景颜色,默认白色
|
||||
blend: 接缝融合过渡区域高度(像素),默认0不融合,推荐30-50
|
||||
|
||||
Returns:
|
||||
包含拼接结果的字典
|
||||
"""
|
||||
if not image_paths:
|
||||
return {"success": False, "error": "没有提供图片路径"}
|
||||
|
||||
valid_paths = []
|
||||
for p in image_paths:
|
||||
if os.path.exists(p):
|
||||
valid_paths.append(p)
|
||||
else:
|
||||
print(f"警告: 文件不存在,跳过 - {p}")
|
||||
|
||||
if not valid_paths:
|
||||
return {"success": False, "error": "没有有效的图片文件"}
|
||||
|
||||
try:
|
||||
imgs = [Image.open(p) for p in valid_paths]
|
||||
|
||||
resized_imgs = []
|
||||
for img in imgs:
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
img = img.convert('RGB')
|
||||
ratio = self.target_width / img.width
|
||||
new_height = int(img.height * ratio)
|
||||
resized = img.resize((self.target_width, new_height), Image.Resampling.LANCZOS)
|
||||
resized_imgs.append(resized)
|
||||
|
||||
if blend > 0 and len(resized_imgs) > 1:
|
||||
for i in range(1, len(resized_imgs)):
|
||||
resized_imgs[i] = self._blend_images(resized_imgs[i-1], resized_imgs[i], blend)
|
||||
|
||||
total_height = sum(img.height for img in resized_imgs) + gap * (len(resized_imgs) - 1)
|
||||
|
||||
long_image = Image.new('RGB', (self.target_width, total_height), background_color)
|
||||
|
||||
y_offset = 0
|
||||
for img in resized_imgs:
|
||||
long_image.paste(img, (0, y_offset))
|
||||
y_offset += img.height + gap
|
||||
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
long_image.save(output_path, quality=95)
|
||||
|
||||
for img in imgs:
|
||||
img.close()
|
||||
for img in resized_imgs:
|
||||
img.close()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"saved_path": output_path,
|
||||
"width": self.target_width,
|
||||
"height": total_height,
|
||||
"image_count": len(resized_imgs)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def merge_from_pattern(
|
||||
self,
|
||||
pattern: str,
|
||||
output_path: str,
|
||||
sort_by: str = "name",
|
||||
gap: int = 0,
|
||||
background_color: str = "white",
|
||||
blend: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
通过 glob 模式匹配图片并拼接
|
||||
|
||||
Args:
|
||||
pattern: glob 模式,如 "*.png" 或 "generated_*.png"
|
||||
output_path: 输出文件路径
|
||||
sort_by: 排序方式 - "name"(文件名) / "time"(修改时间) / "none"(不排序)
|
||||
gap: 图片间隔
|
||||
background_color: 背景颜色
|
||||
blend: 接缝融合过渡高度
|
||||
|
||||
Returns:
|
||||
包含拼接结果的字典
|
||||
"""
|
||||
image_paths = glob_module.glob(pattern)
|
||||
|
||||
if not image_paths:
|
||||
return {"success": False, "error": f"没有找到匹配 '{pattern}' 的图片"}
|
||||
|
||||
if sort_by == "name":
|
||||
image_paths.sort()
|
||||
elif sort_by == "time":
|
||||
image_paths.sort(key=lambda x: os.path.getmtime(x))
|
||||
|
||||
print(f"找到 {len(image_paths)} 张图片:")
|
||||
for i, p in enumerate(image_paths, 1):
|
||||
print(f" {i}. {os.path.basename(p)}")
|
||||
|
||||
return self.merge(image_paths, output_path, gap, background_color, blend)
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行入口"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='长图拼接工具 - 将多张图片垂直拼接成微信长图',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例用法:
|
||||
# 拼接指定图片
|
||||
python merge_long_image.py img1.png img2.png img3.png -o output.png
|
||||
|
||||
# 使用通配符匹配
|
||||
python merge_long_image.py -p "generated_*.png" -o long_image.png
|
||||
|
||||
# 指定宽度和间隔
|
||||
python merge_long_image.py -p "*.png" -o out.png -w 750 -g 20
|
||||
|
||||
# 按修改时间排序
|
||||
python merge_long_image.py -p "*.png" -o out.png --sort time
|
||||
|
||||
# 启用接缝融合过渡(推荐40px)
|
||||
python merge_long_image.py img1.png img2.png -o out.png --blend 40
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('images', nargs='*', help='要拼接的图片路径列表')
|
||||
parser.add_argument('-p', '--pattern', help='glob 模式匹配图片,如 "*.png"')
|
||||
parser.add_argument('-o', '--output', required=True, help='输出文件路径')
|
||||
parser.add_argument('-w', '--width', type=int, default=1080, help='目标宽度,默认1080')
|
||||
parser.add_argument('-g', '--gap', type=int, default=0, help='图片间隔像素,默认0')
|
||||
parser.add_argument('--sort', choices=['name', 'time', 'none'], default='name',
|
||||
help='排序方式:name(文件名)/time(修改时间)/none')
|
||||
parser.add_argument('--bg', default='white', help='背景颜色,默认 white')
|
||||
parser.add_argument('--blend', type=int, default=0,
|
||||
help='接缝融合过渡高度(像素),推荐30-50,默认0不融合')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.images and not args.pattern:
|
||||
parser.error("请提供图片路径列表或使用 -p 指定匹配模式")
|
||||
|
||||
merger = LongImageMerger(target_width=args.width)
|
||||
|
||||
if args.pattern:
|
||||
result = merger.merge_from_pattern(
|
||||
pattern=args.pattern,
|
||||
output_path=args.output,
|
||||
sort_by=args.sort,
|
||||
gap=args.gap,
|
||||
background_color=args.bg,
|
||||
blend=args.blend
|
||||
)
|
||||
else:
|
||||
result = merger.merge(
|
||||
image_paths=args.images,
|
||||
output_path=args.output,
|
||||
gap=args.gap,
|
||||
background_color=args.bg,
|
||||
blend=args.blend
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
print(f"\n拼接成功!")
|
||||
print(f"输出文件: {result['saved_path']}")
|
||||
print(f"尺寸: {result['width']} x {result['height']}")
|
||||
print(f"共 {result['image_count']} 张图片")
|
||||
else:
|
||||
print(f"\n拼接失败: {result['error']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user