← 返回列表

图片批量压缩与裁剪

利用 Pillow (PIL) 图像处理库,对文件夹中的高分辨率图片进行批量压缩和尺寸调整,以便于网页加载或存储优化,保持画质与体积的平衡。

快速预览:

from PIL import Image
img = Image.open('photo.jpg')
# 调整尺寸为 800x600
img = img.resize((800, 600))
# 保存并设置压缩质量
img.save('photo_optimized.jpg', quality=85)

应用场景与价值

在数字化时代,图片处理是一项常见的需求。批量图片压缩与裁剪工具在以下场景中极具价值:

  • 网站优化: 压缩网页图片,加快加载速度,提升用户体验和 SEO 排名
  • 存储管理: 减小照片文件大小,节省云存储空间和本地硬盘
  • 社交媒体: 批量调整图片尺寸以符合平台要求(Instagram、微信朋友圈等)
  • 电商运营: 统一商品图片尺寸,生成缩略图和详情图
  • 摄影工作流: 批量处理拍摄的照片,生成预览图和打印版
  • 移动应用: 压缩图片资源,减小 APP 安装包体积

手动处理大量图片费时费力,使用 Python 的 Pillow 库可以在几分钟内自动处理成百上千张图片,保持高质量的同时显著减小文件体积。

基础知识

Pillow 库

Pillow 是 Python Imaging Library (PIL) 的现代分支,是 Python 最流行的图像处理库。

图片基础概念

  1. 分辨率: 图片的宽度和高度(像素数),如 1920x1080
  2. 文件格式: JPEG、PNG、GIF、BMP、WebP 等
  3. 色彩模式: RGB(彩色)、RGBA(带透明度)、L(灰度)等
  4. 压缩质量: JPEG 的压缩程度,通常 1-100,值越大质量越高

常用图片格式对比

格式 特点 适用场景
JPEG 有损压缩,体积小 照片、复杂图像
PNG 无损压缩,支持透明 Logo、图标、需要透明度的图片
WebP Google 开发,体积更小 现代网站优化
GIF 支持动画 简单动画

Pillow 核心操作

from PIL import Image

# 打开图片
img = Image.open('photo.jpg')

# 获取图片信息
print(img.size)      # (宽度, 高度)
print(img.format)    # 'JPEG'
print(img.mode)      # 'RGB'

# 调整尺寸
img_resized = img.resize((800, 600))

# 裁剪
img_cropped = img.crop((100, 100, 500, 500))  # (left, top, right, bottom)

# 旋转
img_rotated = img.rotate(90)

# 保存
img.save('output.jpg', quality=85)

代码详解

让我们深入理解图片压缩的核心代码:

from PIL import Image

导入 Pillow 库的 Image 模块。如果未安装,使用 uv add pillow 安装。

img = Image.open('photo.jpg')

打开并加载图片到内存。Pillow 会自动识别图片格式。

img = img.resize((800, 600))

调整图片尺寸为 800x600 像素。默认使用高质量的重采样算法(LANCZOS)。注意这会改变图片的宽高比,可能导致变形。

img.save('photo_optimized.jpg', quality=85)

保存压缩后的图片。quality 参数控制 JPEG 压缩质量(1-100): - 95-100: 极高质量,文件较大 - 85-94: 高质量,适合大多数场景(推荐) - 75-84: 中等质量,文件较小 - 60-74: 低质量,明显失真

完整代码

基础版本:批量压缩图片

from PIL import Image
import os

def batch_compress_images(input_folder, output_folder, quality=85, max_size=None):
    """
    批量压缩图片

    参数:
        input_folder: 输入文件夹路径
        output_folder: 输出文件夹路径
        quality: JPEG 压缩质量(1-100)
        max_size: 最大尺寸 (宽度, 高度),None 表示保持原尺寸
    """
    # 创建输出文件夹
    os.makedirs(output_folder, exist_ok=True)

    # 支持的图片格式
    supported_formats = ('.jpg', '.jpeg', '.png', '.bmp', '.gif')

    processed_count = 0
    total_original_size = 0
    total_compressed_size = 0

    for filename in os.listdir(input_folder):
        if not filename.lower().endswith(supported_formats):
            continue

        input_path = os.path.join(input_folder, filename)
        output_path = os.path.join(output_folder, filename)

        try:
            # 获取原始文件大小
            original_size = os.path.getsize(input_path)
            total_original_size += original_size

            # 打开图片
            with Image.open(input_path) as img:
                # 转换 RGBA 或 P 模式到 RGB(JPEG 不支持透明度)
                if img.mode in ('RGBA', 'P'):
                    img = img.convert('RGB')

                # 调整尺寸(如果指定)
                if max_size:
                    img.thumbnail(max_size, Image.Resampling.LANCZOS)

                # 保存压缩图片
                img.save(output_path, 'JPEG', quality=quality, optimize=True)

            # 获取压缩后文件大小
            compressed_size = os.path.getsize(output_path)
            total_compressed_size += compressed_size

            reduction = (1 - compressed_size / original_size) * 100
            print(f"处理: {filename} | 原始: {original_size / 1024:.1f}KB -> 压缩: {compressed_size / 1024:.1f}KB | 减少: {reduction:.1f}%")

            processed_count += 1

        except Exception as e:
            print(f"处理 {filename} 失败: {e}")

    # 输出总结
    print("\n" + "=" * 60)
    print(f"批量压缩完成!")
    print(f"处理图片数: {processed_count}")
    print(f"总原始大小: {total_original_size / 1024 / 1024:.2f} MB")
    print(f"总压缩大小: {total_compressed_size / 1024 / 1024:.2f} MB")
    print(f"总体积减少: {(1 - total_compressed_size / total_original_size) * 100:.1f}%")
    print("=" * 60)


# 使用示例
if __name__ == "__main__":
    batch_compress_images(
        input_folder='./original_photos',
        output_folder='./compressed_photos',
        quality=85,
        max_size=(1920, 1080)  # 限制最大尺寸
    )

进阶版本:智能裁剪与缩略图生成

from PIL import Image
import os

def create_thumbnails(input_folder, output_folder, sizes):
    """
    生成多种尺寸的缩略图

    参数:
        sizes: 尺寸列表,如 [(200, 200), (400, 400), (800, 800)]
    """
    os.makedirs(output_folder, exist_ok=True)

    for filename in os.listdir(input_folder):
        if not filename.lower().endswith(('.jpg', '.jpeg', '.png')):
            continue

        input_path = os.path.join(input_folder, filename)
        name, ext = os.path.splitext(filename)

        try:
            with Image.open(input_path) as img:
                if img.mode in ('RGBA', 'P'):
                    img = img.convert('RGB')

                for width, height in sizes:
                    # 智能裁剪:保持宽高比的中心裁剪
                    img_copy = img.copy()
                    img_copy.thumbnail((width * 2, height * 2), Image.Resampling.LANCZOS)

                    # 计算裁剪位置(居中)
                    left = (img_copy.width - width) // 2
                    top = (img_copy.height - height) // 2
                    right = left + width
                    bottom = top + height

                    img_cropped = img_copy.crop((left, top, right, bottom))

                    # 保存缩略图
                    output_filename = f"{name}_{width}x{height}{ext}"
                    output_path = os.path.join(output_folder, output_filename)
                    img_cropped.save(output_path, quality=85, optimize=True)

                    print(f"生成缩略图: {output_filename}")

        except Exception as e:
            print(f"处理 {filename} 失败: {e}")


# 使用示例
create_thumbnails(
    input_folder='./photos',
    output_folder='./thumbnails',
    sizes=[(200, 200), (400, 400), (800, 800)]
)

高级版本:保持宽高比的智能压缩

from PIL import Image
import os

def smart_resize(img, max_width, max_height):
    """
    智能调整尺寸:保持宽高比,不超过最大尺寸

    返回:
        调整后的图片对象
    """
    original_width, original_height = img.size

    # 计算缩放比例
    width_ratio = max_width / original_width
    height_ratio = max_height / original_height
    scale = min(width_ratio, height_ratio, 1.0)  # 不放大图片

    if scale < 1.0:
        new_width = int(original_width * scale)
        new_height = int(original_height * scale)
        img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

    return img


def advanced_batch_process(input_folder, output_folder, max_width=1920,
                           max_height=1080, quality=85, format_convert='JPEG'):
    """
    高级批量图片处理

    参数:
        format_convert: 目标格式('JPEG', 'PNG', 'WebP')
    """
    os.makedirs(output_folder, exist_ok=True)

    for filename in os.listdir(input_folder):
        if not filename.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')):
            continue

        input_path = os.path.join(input_folder, filename)
        name, _ = os.path.splitext(filename)

        # 根据目标格式设置扩展名
        ext_map = {'JPEG': '.jpg', 'PNG': '.png', 'WebP': '.webp'}
        new_ext = ext_map.get(format_convert, '.jpg')
        output_filename = name + new_ext
        output_path = os.path.join(output_folder, output_filename)

        try:
            with Image.open(input_path) as img:
                # 处理透明度
                if format_convert == 'JPEG' and img.mode in ('RGBA', 'P'):
                    # JPEG 不支持透明,创建白色背景
                    background = Image.new('RGB', img.size, (255, 255, 255))
                    background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
                    img = background
                elif img.mode not in ('RGB', 'RGBA'):
                    img = img.convert('RGB')

                # 智能调整尺寸
                img = smart_resize(img, max_width, max_height)

                # 保存
                save_kwargs = {'optimize': True}
                if format_convert in ('JPEG', 'WebP'):
                    save_kwargs['quality'] = quality

                img.save(output_path, format_convert, **save_kwargs)

                original_size = os.path.getsize(input_path) / 1024
                compressed_size = os.path.getsize(output_path) / 1024
                print(f"{filename} -> {output_filename} | {original_size:.1f}KB -> {compressed_size:.1f}KB")

        except Exception as e:
            print(f"处理 {filename} 失败: {e}")


# 使用示例
advanced_batch_process(
    input_folder='./original',
    output_folder='./optimized',
    max_width=1920,
    max_height=1080,
    quality=85,
    format_convert='JPEG'
)

添加水印功能

from PIL import Image, ImageDraw, ImageFont

def add_watermark(img, text, position='bottom-right', opacity=128):
    """
    为图片添加文字水印

    参数:
        text: 水印文字
        position: 位置 ('top-left', 'top-right', 'bottom-left', 'bottom-right', 'center')
        opacity: 不透明度 (0-255)
    """
    # 创建透明图层
    watermark_layer = Image.new('RGBA', img.size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(watermark_layer)

    # 设置字体(如果系统字体不可用,会使用默认字体)
    try:
        font = ImageFont.truetype('Arial.ttf', 36)
    except:
        font = ImageFont.load_default()

    # 获取文本大小
    bbox = draw.textbbox((0, 0), text, font=font)
    text_width = bbox[2] - bbox[0]
    text_height = bbox[3] - bbox[1]

    # 计算位置
    margin = 20
    positions = {
        'top-left': (margin, margin),
        'top-right': (img.width - text_width - margin, margin),
        'bottom-left': (margin, img.height - text_height - margin),
        'bottom-right': (img.width - text_width - margin, img.height - text_height - margin),
        'center': ((img.width - text_width) // 2, (img.height - text_height) // 2)
    }
    xy = positions.get(position, positions['bottom-right'])

    # 绘制水印
    draw.text(xy, text, fill=(255, 255, 255, opacity), font=font)

    # 合并图层
    img_with_watermark = Image.alpha_composite(img.convert('RGBA'), watermark_layer)
    return img_with_watermark.convert('RGB')


# 使用示例
img = Image.open('photo.jpg')
img_watermarked = add_watermark(img, '© 2026 Your Name', position='bottom-right')
img_watermarked.save('photo_watermarked.jpg', quality=90)

运行示例

安装依赖

uv add pillow

准备测试图片

将需要处理的图片放入 original_photos 文件夹。

执行压缩

uv run main.py

预期输出

处理: photo1.jpg | 原始: 3520.5KB -> 压缩: 452.3KB | 减少: 87.1%
处理: photo2.jpg | 原始: 2890.2KB -> 压缩: 380.7KB | 减少: 86.8%
处理: photo3.png | 原始: 5120.8KB -> 压缩: 620.5KB | 减少: 87.9%
...

============================================================
批量压缩完成!
处理图片数: 20
总原始大小: 68.45 MB
总压缩大小: 8.92 MB
总体积减少: 87.0%
============================================================

扩展思路

1. 图片格式转换

def convert_to_webp(input_folder, output_folder, quality=80):
    """批量转换为 WebP 格式"""
    for filename in os.listdir(input_folder):
        if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
            input_path = os.path.join(input_folder, filename)
            output_path = os.path.join(output_folder, os.path.splitext(filename)[0] + '.webp')

            with Image.open(input_path) as img:
                img.save(output_path, 'WebP', quality=quality)

2. 图片滤镜效果

from PIL import ImageFilter, ImageEnhance

# 锐化
img_sharp = img.filter(ImageFilter.SHARPEN)

# 模糊
img_blur = img.filter(ImageFilter.GaussianBlur(radius=2))

# 调整亮度
enhancer = ImageEnhance.Brightness(img)
img_bright = enhancer.enhance(1.5)  # 1.5倍亮度

# 调整对比度
enhancer = ImageEnhance.Contrast(img)
img_contrast = enhancer.enhance(1.3)

# 转灰度
img_gray = img.convert('L')

3. 智能内容识别裁剪

使用人脸识别库进行智能裁剪:

# 需要安装: uv add opencv-python
import cv2

def smart_crop_with_face_detection(image_path, output_size):
    """基于人脸检测的智能裁剪"""
    img = cv2.imread(image_path)
    face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
    faces = face_cascade.detectMultiScale(img, 1.1, 4)

    if len(faces) > 0:
        # 以检测到的人脸为中心裁剪
        x, y, w, h = faces[0]
        center_x, center_y = x + w // 2, y + h // 2
        # 裁剪逻辑...

4. 批量添加边框

from PIL import ImageOps

def add_border(img, border_size=10, color='white'):
    """为图片添加边框"""
    return ImageOps.expand(img, border=border_size, fill=color)

5. 图片质量评估

def estimate_quality(image_path):
    """估算图片质量(基于文件大小和分辨率)"""
    img = Image.open(image_path)
    file_size = os.path.getsize(image_path)
    pixels = img.width * img.height
    bytes_per_pixel = file_size / pixels

    if bytes_per_pixel > 3:
        return "高质量"
    elif bytes_per_pixel > 1.5:
        return "中等质量"
    else:
        return "低质量"

6. 生成图片网格

def create_image_grid(image_paths, grid_size=(3, 3), thumbnail_size=(300, 300)):
    """将多张图片组合成网格"""
    cols, rows = grid_size
    width, height = thumbnail_size

    grid_img = Image.new('RGB', (cols * width, rows * height), 'white')

    for i, path in enumerate(image_paths[:cols * rows]):
        img = Image.open(path)
        img.thumbnail(thumbnail_size, Image.Resampling.LANCZOS)

        col = i % cols
        row = i // cols
        grid_img.paste(img, (col * width, row * height))

    return grid_img

7. EXIF 数据处理

from PIL.ExifTags import TAGS

def read_exif(image_path):
    """读取图片 EXIF 信息"""
    img = Image.open(image_path)
    exif_data = img._getexif()

    if exif_data:
        for tag_id, value in exif_data.items():
            tag = TAGS.get(tag_id, tag_id)
            print(f"{tag}: {value}")

def remove_exif(image_path, output_path):
    """移除图片 EXIF 信息(保护隐私)"""
    img = Image.open(image_path)
    data = list(img.getdata())
    image_without_exif = Image.new(img.mode, img.size)
    image_without_exif.putdata(data)
    image_without_exif.save(output_path)

注意事项

图片质量

  1. 质量参数选择: 85-90 是平衡质量和体积的最佳值
  2. 避免重复压缩: 已压缩的图片不应再次压缩,会导致质量严重下降
  3. 原始备份: 保留原始高质量图片,避免不可逆损失

性能优化

  1. 批量处理: 一次处理多张图片时考虑使用多进程
  2. 内存管理: 处理大图时及时关闭图片对象释放内存
  3. 缓存机制: 避免重复处理已处理的图片

格式选择

  • JPEG: 照片、复杂图像(有损压缩)
  • PNG: 需要透明度、简单图形(无损压缩)
  • WebP: 现代网站(更小体积,需检查浏览器兼容性)

版权与隐私

  1. 水印保护: 为原创图片添加水印防止盗用
  2. EXIF 数据: 移除敏感位置信息保护隐私
  3. 版权声明: 确保有权处理和使用这些图片

通过掌握 Pillow 图片处理技术,你可以高效地进行图片批量优化、创建自动化的图片处理流程,为网站性能优化、内容管理和创意设计提供强大支持。