文章目录
- 依赖
- Code
- 效果
D指导帮写的,挺好用,发出来备用;
依赖
bash
pip3 install Pillow
Code
python3
from PIL import Image, ImageSequence, ImagePalette
import argparse
import sys
import os
import numpy as np
from collections import Counter
def resize_gif_with_alpha(input_path, output_path, target_size=None, scale=None, dither='NONE'):
"""
高质量缩放GIF图片 - 专门优化透明度处理
:param input_path: 输入GIF文件路径
:param output_path: 输出GIF文件路径
:param target_size: 目标尺寸元组 (width, height)
:param scale: 缩放比例 (0.0-1.0为缩小, >1.0为放大)
:param dither: 抖动方法 (NONE, FLOYDSTEINBERG)
"""
# 验证参数
if not (target_size or scale):
raise ValueError("必须提供 target_size 或 scale 参数")
if target_size and scale:
raise ValueError("不能同时提供 target_size 和 scale")
# 打开GIF
try:
with Image.open(input_path) as gif:
# 获取原始GIF信息
loop = gif.info.get('loop', 0)
background = gif.info.get('background', 0)
transparency = gif.info.get('transparency', None)
duration = gif.info.get('duration', 100)
# 计算目标尺寸
if target_size:
new_size = tuple(target_size)
else:
original_size = gif.size
new_size = (int(original_size[0] * scale), int(original_size[1] * scale))
# 创建帧列表
frames = []
durations = []
disposal_methods = []
# 收集所有帧数据
all_frames = []
all_alphas = []
print(f"🔍 分析GIF: {input_path}")
print(f" 原始尺寸: {gif.size} → 目标尺寸: {new_size}")
# 第一步: 收集所有帧
for frame_idx, frame in enumerate(ImageSequence.Iterator(gif)):
# 获取帧信息
frame_duration = frame.info.get('duration', duration)
disposal = frame.disposal_method if hasattr(frame, 'disposal_method') else 2
# 转换帧格式,确保正确处理透明度
if frame.mode == 'P':
# 调色板模式,检查是否有透明度
if 'transparency' in frame.info:
# 转换为RGBA以保留透明度
frame_rgba = frame.convert('RGBA')
# 分离颜色和alpha
rgb_frame = frame_rgba.convert('RGB')
alpha_frame = frame_rgba.split()[3]
else:
# 无透明度,转换为RGB
rgb_frame = frame.convert('RGB')
alpha_frame = None
elif frame.mode == 'RGBA':
# 已经是RGBA模式
rgb_frame = frame.convert('RGB')
alpha_frame = frame.split()[3]
else:
# 其他模式
rgb_frame = frame.convert('RGB')
alpha_frame = None
# 缩放帧
resized_rgb = rgb_frame.resize(new_size, Image.LANCZOS)
resized_alpha = alpha_frame.resize(new_size, Image.LANCZOS) if alpha_frame else None
all_frames.append(resized_rgb)
all_alphas.append(resized_alpha)
durations.append(frame_duration)
disposal_methods.append(disposal)
print(f" 总帧数: {len(all_frames)}")
print(f" 透明帧: {sum(1 for a in all_alphas if a is not None)}")
# 第二步: 分析透明度需求
has_transparency = any(alpha is not None for alpha in all_alphas)
if has_transparency:
print("🌫️ 检测到透明区域,创建透明优化调色板...")
# 创建支持透明度的调色板
palette_data, transparency_index = create_transparency_optimized_palette(all_frames, all_alphas)
else:
print("🎨 创建标准调色板...")
# 创建标准调色板
palette_data = create_color_preserving_palette(all_frames)
transparency_index = None
# 创建调色板图像
palette_image = Image.new('P', (1, 1), 0)
palette_image.putpalette(palette_data)
# 第三步: 处理每一帧
print("🖼️ 处理帧并应用调色板...")
for idx, (frame, alpha) in enumerate(zip(all_frames, all_alphas)):
try:
# 应用调色板
if has_transparency and alpha is not None:
# 处理透明帧
quantized_frame = apply_palette_with_alpha(
frame,
palette_image,
alpha,
transparency_index,
dither_mode=dither
)
else:
# 处理不透明帧
quantized_frame = apply_palette_without_alpha(
frame,
palette_image,
dither_mode=dither
)
frames.append(quantized_frame)
# 进度反馈
if (idx + 1) % 10 == 0 or (idx + 1) == len(all_frames):
print(f" 已处理: {idx+1}/{len(all_frames)} 帧")
except Exception as e:
print(f"⚠️ 帧 {idx} 处理错误: {e}, 跳过此帧")
continue
# 第四步: 保存GIF
if frames:
save_args = {
'save_all': True,
'append_images': frames[1:],
'duration': durations[:len(frames)],
'loop': loop,
'background': background,
'optimize': True
}
# 添加透明度信息
if transparency_index is not None:
save_args['transparency'] = transparency_index
# 添加disposal方法
if any(d != 0 for d in disposal_methods[:len(frames)]):
save_args['disposal'] = disposal_methods[:len(frames)]
frames[0].save(
output_path,
format='GIF',
**save_args
)
print(f"\n✅ 成功缩放GIF! 新尺寸: {new_size}")
print(f"📂 输出文件: {os.path.abspath(output_path)}")
print(f"🖼️ 输出帧数: {len(frames)}")
if transparency_index is not None:
print(f"🌫️ 透明度索引: {transparency_index}")
return True
else:
print("❌ 未找到有效帧,未创建输出文件")
return False
except Exception as e:
print(f"❌ 处理出错: {e}")
import traceback
traceback.print_exc()
return False
def create_transparency_optimized_palette(rgb_frames, alpha_frames):
"""
创建支持透明度的调色板
保留一个颜色索引给完全透明像素
"""
# 收集所有不透明像素的颜色
opaque_colors = []
for rgb_frame, alpha_frame in zip(rgb_frames, alpha_frames):
# 将帧转换为numpy数组以提高性能
rgb_array = np.array(rgb_frame)
if alpha_frame is not None:
# 如果有alpha通道,只收集不透明像素
alpha_array = np.array(alpha_frame)
opaque_mask = alpha_array > 128 # 半透明以上视为不透明
if np.any(opaque_mask):
opaque_pixels = rgb_array[opaque_mask]
opaque_colors.extend([tuple(pixel) for pixel in opaque_pixels])
else:
# 没有alpha通道,收集所有像素
pixels = rgb_array.reshape(-1, 3)
opaque_colors.extend([tuple(pixel) for pixel in pixels])
if not opaque_colors:
# 如果没有不透明像素,创建默认调色板
print("⚠️ 没有不透明像素,使用默认调色板")
palette_data = create_default_palette()
return palette_data, 0
# 统计颜色频率
color_counter = Counter(opaque_colors)
# 如果颜色数量超过255,需要进行量化
if len(color_counter) > 255:
print(f" 颜色数量 ({len(color_counter)}) 超过255,进行量化...")
# 使用Pillow量化算法
color_image = Image.new('RGB', (len(opaque_colors), 1))
# 填充颜色
for i, color in enumerate(opaque_colors[:len(opaque_colors)]):
color_image.putpixel((i, 0), color)
# 量化到254种颜色(留1个给透明)
quantized = color_image.quantize(
colors=254,
method=Image.Quantize.MEDIANCUT,
kmeans=0,
dither=Image.Dither.NONE
)
# 获取调色板
palette = quantized.getpalette()
if palette is None:
# 如果获取调色板失败,使用默认
palette_data = create_default_palette()
else:
# 确保调色板是768字节(256色×3)
palette_data = bytearray(palette[:768])
else:
# 颜色数量不超过255,使用实际颜色
print(f" 使用 {len(color_counter)} 种独特颜色")
# 创建调色板
palette_data = bytearray(768) # 256色×3字节
# 填充前N种颜色
unique_colors = list(dict(color_counter.most_common(255))) # 最多255种
for i, color in enumerate(unique_colors):
idx = i * 3
palette_data[idx] = color[0] # R
palette_data[idx+1] = color[1] # G
palette_data[idx+2] = color[2] # B
# 确保调色板长度为768字节
if len(palette_data) < 768:
palette_data.extend(bytes(768 - len(palette_data)))
# 将最后一个颜色索引(255)保留给透明
# 设置透明色为黑色(0,0,0) - 在GIF中这个颜色不会显示
palette_data[765] = 0 # R
palette_data[766] = 0 # G
palette_data[767] = 0 # B
return bytes(palette_data), 255 # 透明度索引为255
def create_default_palette():
"""创建默认的256色调色板"""
palette_data = bytearray(768)
# 标准216色网页安全调色板
web_safe_colors = []
for r in range(0, 256, 51): # 0, 51, 102, 153, 204, 255
for g in range(0, 256, 51):
for b in range(0, 256, 51):
web_safe_colors.extend([r, g, b])
# 填充前216个颜色
palette_data[:len(web_safe_colors)] = web_safe_colors
# 其余颜色填充灰度渐变
for i in range(216, 256):
idx = i * 3
gray = int((i - 216) * 255 / 40)
palette_data[idx] = gray
palette_data[idx+1] = gray
palette_data[idx+2] = gray
return bytes(palette_data)
def create_color_preserving_palette(frames):
"""
创建保留鲜艳颜色的调色板(无透明度)
"""
# 创建一个大的RGB图像来收集所有帧的颜色
width, height = frames[0].size
combined_img = Image.new('RGB', (width, height * len(frames)))
# 粘贴所有帧到合并图像中
for i, frame in enumerate(frames):
combined_img.paste(frame, (0, i * height))
# 使用自适应量化算法保留鲜艳颜色
optimized_img = combined_img.quantize(
colors=256,
method=Image.Quantize.MEDIANCUT,
kmeans=0,
dither=Image.Dither.NONE
)
# 获取调色板
palette = optimized_img.getpalette()
if palette is None:
return create_default_palette()
return palette[:768] # 确保返回768字节
def apply_palette_with_alpha(rgb_image, palette_image, alpha_channel, transparency_index, dither_mode='NONE'):
"""
应用调色板到RGB图像,并处理透明度
"""
# 先将RGB图像量化到调色板
dither_method = Image.Dither.NONE if dither_mode == 'NONE' else Image.Dither.FLOYDSTEINBERG
# 使用提供的调色板进行量化
quantized = rgb_image.quantize(
palette=palette_image,
dither=dither_method
)
# 将量化后的图像转换为P模式
quantized = quantized.convert('P', palette=palette_image, colors=256)
# 将alpha通道转换为二值掩码
# 阈值128:alpha > 128 视为不透明,否则视为透明
alpha_mask = alpha_channel.point(lambda x: 0 if x > 128 else 255)
# 创建最终图像
final_image = Image.new('P', rgb_image.size)
final_image.putpalette(palette_image.getpalette())
# 复制像素数据
quantized_data = list(quantized.getdata())
mask_data = list(alpha_mask.getdata())
# 应用透明度:透明像素设置为透明度索引
final_data = []
for q_pixel, m_pixel in zip(quantized_data, mask_data):
if m_pixel == 255: # 透明
final_data.append(transparency_index)
else: # 不透明
final_data.append(q_pixel)
final_image.putdata(final_data)
return final_image
def apply_palette_without_alpha(rgb_image, palette_image, dither_mode='NONE'):
"""
应用调色板到RGB图像(无透明度)
"""
dither_method = Image.Dither.NONE if dither_mode == 'NONE' else Image.Dither.FLOYDSTEINBERG
# 使用提供的调色板进行量化
quantized = rgb_image.quantize(
palette=palette_image,
dither=dither_method
)
return quantized
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='高质量GIF缩放工具 - 优化透明度处理',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('input', help='输入GIF文件路径')
parser.add_argument('output', help='输出GIF文件路径')
size_group = parser.add_mutually_exclusive_group(required=True)
size_group.add_argument('-s', '--scale', type=float, default=None,
help='缩放比例 (示例: 0.5=缩小一半, 2.0=放大一倍)')
size_group.add_argument('-t', '--target', type=int, nargs=2,
metavar=('WIDTH', 'HEIGHT'), default=None,
help='目标尺寸 (宽度 高度)')
parser.add_argument('-d', '--dither', type=str, default='NONE',
choices=['NONE', 'FLOYDSTEINBERG'],
help='抖动方法 (NONE=无抖动减少噪点, FLOYDSTEINBERG=有抖动平滑颜色)')
parser.add_argument('-a', '--alpha-threshold', type=int, default=128,
help='透明度阈值 (0-255, 大于此值的alpha视为不透明)')
args = parser.parse_args()
# 注意:alpha_threshold参数在当前版本中尚未集成到主函数
# 可以根据需要修改create_transparency_optimized_palette函数使用此阈值
# 执行缩放并返回状态
result = resize_gif_with_alpha(
args.input,
args.output,
target_size=args.target,
scale=args.scale,
dither=args.dither
)
if not result:
sys.exit(1)
else:
sys.exit(0)
效果
bash
python gif_resizer_rgba.py 5.gif output_5.gif --scale 0.2
🔍 分析GIF: 5.gif
原始尺寸: (720, 720) → 目标尺寸: (144, 144)
总帧数: 9
透明帧: 9
🌫️ 检测到透明区域,创建透明优化调色板...
颜色数量 (1785) 超过255,进行量化...
🖼️ 处理帧并应用调色板...
已处理: 9/9 帧
✅ 成功缩放GIF! 新尺寸: (144, 144)
📂 输出文件: /home/ppmitest/workspace/python/output_5.gif
🖼️ 输出帧数: 9
🌫️ 透明度索引: 255
原图:

缩放后:
