python实现gif图片缩放,支持透明像素

文章目录

  • 依赖
  • 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

原图:

缩放后:

相关推荐
q_30238195562 小时前
14.7MB轻量模型!NVIDIA Jetson边缘设备解锁工厂设备故障预警新方案
人工智能·python·算法·ascend·算子开发
Niuguangshuo2 小时前
PyTorch优化器完全指南
人工智能·pytorch·python
3824278272 小时前
python:yield用法
开发语言·python
STLearner2 小时前
AAAI 2026 | 时空数据(Spatial-temporal)论文总结[下](自动驾驶,天气预报,城市科学,POI推荐等)
人工智能·python·深度学习·机器学习·数据挖掘·自动驾驶·智慧城市
郝学胜-神的一滴3 小时前
人工智能与机器学习:从理论到实践的技术全景
人工智能·python·程序人生·算法·机器学习
长安牧笛3 小时前
开发中老年发型设计推荐系统,输入脸型,年龄,推荐适合的发型,提供效果图参考。
python
superman超哥3 小时前
仓颉内存分配优化深度解析
c语言·开发语言·c++·python·仓颉
一车小面包3 小时前
大模型与检索系统集成开发核心知识点总结
python
2401_841495643 小时前
并行程序设计与实现
c++·python·算法·cuda·mpi·并行计算·openmp