Files
daily-opencode-workspace/.opencode/skills/image-service/scripts/merge_long_image.py

252 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()