本节摘要:裁剪、缩放、旋转、翻转是图像处理最常用的四把"剪刀"。本节逐一拆解 crop、resize、thumbnail、rotate、transpose 的用法与参数,重点讲清 resize 与 thumbnail 的区别、rotate 的 expand 参数、以及坐标系(左上角原点)的理解。
阅读完本节,你应当能够:
图像处理里的坐标和数学课不一样:原点在左上角,x 向右增大,y 向下增大。一张 100x80 的图,右下角坐标是 (99, 79)。这个"翻转的 y 轴"是新手第一个容易懵的点。
from PIL import Image img = Image.open("photo.jpg") # 裁剪:左上角 (50, 50) 到右下角 (200, 150) 的矩形 crop_box = (50, 50, 200, 150) cropped = img.crop(crop_box)
crop 的参数是 (left, upper, right, lower)——记住前两个是左上角、后两个是右下角,right/lower 是开区间(不包含边界像素)。
# 切出右半部分 w, h = img.size right_half = img.crop((w // 2, 0, w, h)) # 中心裁一块正方形 side = min(w, h) left = (w - side) // 2 top = (h - side) // 2 center_square = img.crop((left, top, left + side, top + side))
裁剪不改变原图(返回新对象),常用于"去掉无关区域"或"统一构图"。
# resize:强制缩放到指定尺寸(可能变形) img_resized = img.resize((500, 300)) # thumbnail:等比例缩放到不超出指定框(不变形,原地修改) img.thumbnail((500, 300)) print(img.size) # 实际可能是 (500, 281),按比例适配
| 对比 | resize | thumbnail |
|---|---|---|
| 变形 | 会(强制尺寸) | 不会(等比) |
| 放大 | 支持 | 只缩小不放大 |
| 原图 | 不变(新对象) | 原地修改 |
| 用途 | 精确尺寸 | 缩略图、预览图 |
💡 关键直觉:缩略图用 thumbnail,精确尺寸用 resize。Web 场景最常见的是"生成不超框的缩略图",thumbnail 一行解决,还不会拉变形。
# 逆时针旋转 45 度 rotated = img.rotate(45) # 旋转后自动扩展画布(不放缩内容、不裁边) rotated_full = img.rotate(45, expand=True) # 旋转并同时放大(改变输出尺寸) rotated_zoom = img.rotate(45, expand=True, resample=Image.Resampling.BICUBIC)
rotate 默认不扩展画布——45 度旋转后四角会被裁掉。想保留完整内容用 expand=True。注意 rotate 是逆时针。
img_flip_lr = img.transpose(Image.Transpose.FLIP_LEFT_RIGHT) # 水平翻转(镜像) img_flip_tb = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) # 垂直翻转 img_rot_90 = img.transpose(Image.Transpose.ROTATE_90) # 顺时针 90° img_rot_180 = img.transpose(Image.Transpose.ROTATE_180) # 180° img_rot_270 = img.transpose(Image.Transpose.ROTATE_270) # 顺时针 270°
transpose 只做 90° 倍数的旋转,但速度快、不重采样,适合固定角度的翻转。
| 操作 | 常用参数 | 关键注意 |
|---|---|---|
| crop | box 四元组 | right/lower 开区间 |
| resize | size 二元组 | 会变形,可传 resample |
| thumbnail | size 二元组 | 原地修改、只缩不放 |
| rotate | angle, expand | 逆时针;expand 防裁边 |
| transpose | 翻转/90°倍旋转 | 不重采样,速度快 |
⚠️ 常见坑 1:resize 后图片模糊。默认用最近邻插值,放大时颗粒感强。放大图片用
resample=Image.Resampling.LANCZOS(高质量但慢)或 BICUBIC(均衡)。
⚠️ 常见坑 2:旋转 90° 与 transpose 的困惑。
img.rotate(90)是逆时针 90°,img.transpose(ROTATE_90)是顺时针 90°——方向相反!统一用 transpose 处理 90° 倍数旋转最不易错。
把四个操作组合起来,生成一个四宫格拼图,直观看到每个操作的效果:
from PIL import Image img = Image.open("photo.jpg") w, h = img.size # 左上:裁剪中心正方形 side = min(w, h) c = img.crop(((w-side)//2, (h-side)//2, (w+side)//2, (h+side)//2)) # 右上:缩略图 th = img.copy() th.thumbnail((side, side)) # 左下:旋转 45 度 r = img.rotate(45, expand=True) r.thumbnail((side, side)) # 右下:水平翻转 flip = img.transpose(Image.Transpose.FLIP_LEFT_RIGHT) flip.thumbnail((side, side)) # 拼成 2x2 画布 canvas = Image.new("RGB", (side*2, side*2), "white") canvas.paste(c, (0, 0)) canvas.paste(th, (side, 0)) canvas.paste(r, (0, side)) canvas.paste(flip, (side, side)) canvas.save("grid_demo.png")
打开 grid_demo.png,四个象限分别是裁剪、缩放、旋转、翻转的效果——一次看到四个操作,记忆深刻。
四宫格用到了 paste,这是合成类操作的核心方法,值得展开:
from PIL import Image # 把一张小图贴到大画布上 canvas = Image.new("RGB", (800, 600), "white") small = Image.open("logo.png").convert("RGBA") # 普通粘贴(左上角定位) canvas.paste(small, (50, 50)) # 带透明通道的粘贴(必须用 mask) canvas.paste(small, (50, 50), mask=small) # 居中定位 w, h = canvas.size sw, sh = small.size x = (w - sw) // 2 y = (h - sh) // 2 canvas.paste(small, (x, y), mask=small)
paste 的三个要点:
paste(img, (x, y)) 的元组是左上角坐标💡 关键直觉:paste 是"贴纸",alpha_composite 是"融合"。简单贴图用 paste;要做半透明叠加(水印、蒙版)用
Image.alpha_composite(第二章 2.3 会用到)。前者快,后者更细腻。
| 场景 | 用到的操作 | 组合方式 |
|---|---|---|
| 头像生成 | crop + resize | 先居中裁剪再缩放 |
| 封面图 | crop + thumbnail | 固定比例裁剪+等比缩 |
| 证件照 | resize + rotate | 尺寸调整+方向修正 |
| 全景拼图 | resize + paste | 统一尺寸后拼接 |
| 风格化 | rotate + transpose | 旋转加镜像 |
| 打印排版 | resize + rotate | 尺寸与方向控制 |
通用套路:几乎所有几何需求都是"裁剪定比例 → 缩放定尺寸 → 旋转/翻转调方向"的组合。把这三种能力练熟,几何操作就通关了。
问:crop 后图片变大了怎么办?
crop 是"取出区域",区域不可能大于原图。觉得"变大"可能是把坐标写反了(right < left),或把 crop 的结果再 resize 了。检查坐标与后续操作。
问:rotate(90) 和 transpose(ROTATE_90) 有什么区别?
rotate(90) 是逆时针 90°,且默认不扩展画布(内容会被裁);transpose(ROTATE_90) 是顺时针 90°,速度更快不重采样。整 90° 旋转优先用 transpose。
问:thumbnail 后图像变模糊?
thumbnail 默认用较快插值。对质量有要求时,先 img = img.convert("RGB") 再用 img.resize((w,h), Image.Resampling.LANCZOS) 手动控制。缩略图用途 BILINEAR 足够,放大用途用 LANCZOS。
问:怎么批量旋转所有图片?
循环 + rotate + save(详见第四章 4.2 批量处理)。重点:每张独立打开处理保存,不要全部加载到内存。
几何变换中 resample(重采样)参数决定像素怎么"算",值得系统了解:
| resample 值 | 全称 | 特点 | 典型用途 |
|---|---|---|---|
| NEAREST | 最近邻 | 快、锯齿、像素风 | 像素艺术、放大测试 |
| BILINEAR | 双线性 | 中速、柔和 | 一般缩小 |
| BICUBIC | 双三次 | 较慢、锐利 | 照片缩放 |
| LANCZOS | 高质量 | 最慢、最平滑 | 高质量输出 |
| BOX | 盒子 | 类似 NEAREST 平均版 | 缩小 |
| HAMMING | 汉明 | 介于线性之间 | 特定场景 |
# rotate/resize 都支持 resample img.rotate(45, resample=Image.Resampling.BICUBIC) img.resize((new_w, new_h), resample=Image.Resampling.LANCZOS)
💡 关键直觉:"放大的图发虚"是插值选的太弱。升级画质用 LANCZOS;"缩小后变糊"是缩得太狠(信息损失)——此时该考虑"先高斯模糊再缩小"(抗锯齿)这类进阶方案。
四宫格是入门,九宫格(3x3 拼贴)是更常用的排版工具:
from PIL import Image img = Image.open("photo.jpg") # 切成 9 个均匀块 w, h = img.size block_w, block_h = w // 3, h // 3 pieces = [] for r in range(3): for c in range(3): box = (c*block_w, r*block_h, (c+1)*block_w, (r+1)*block_h) pieces.append(img.crop(box)) # 打乱顺序重组("拼图"效果) import random random.shuffle(pieces) canvas = Image.new("RGB", (w, h), "white") for r in range(3): for c in range(3): canvas.paste(pieces[r*3+c], (c*block_w, r*block_h)) canvas.save("puzzle_9.jpg")
九宫格切块 + 打乱重组 = "拼图"效果。这个模式在马赛克、拼图游戏、局部处理里都通用——理解 crop 批量切块,就能玩出很多花样。
大规模几何变换(如批量缩略图)时,性能与质量要平衡:
| 场景 | 推荐 resample | 理由 |
|---|---|---|
| 缩略图(缩小) | BILINEAR | 速度快,缩小够用 |
| 头像(放大) | LANCZOS | 质量优先 |
| 批量预览 | BILINEAR | 速度优先 |
| 打印级输出 | LANCZOS | 质量优先 |
| 像素风/占位 | NEAREST | 特效需求 |
批量缩略图的黄金组合:ImageOps.fit(img, size)(内置优化)+ BILINEAR 默认——绝大多数 Web 缩略图场景这个组合就够了。追求极致质量再上 LANCZOS。
减少重复缩放:同一张图要出多个尺寸时,从原图逐次缩放(不要从缩略图再放大)——每次缩放都有信息损失,链式缩放会累积失真。
crop 是"取出区域",区域不可能大于原图。觉得"变大"可能是把坐标写反了(right < left),或把 crop 的结果再 resize 了。检查坐标与后续操作。
rotate(90) 是逆时针 90°,且默认不扩展画布(内容会被裁);transpose(ROTATE_90) 是顺时针 90°,速度更快不重采样。整 90° 旋转优先用 transpose。
thumbnail 默认用较快插值。对质量有要求时,先 img = img.convert("RGB") 再用 img.resize((w,h), Image.Resampling.LANCZOS) 手动控制。缩略图用途 BILINEAR 足够,放大用途用 LANCZOS。
循环 + rotate + save(详见第四章 4.2 批量处理)。重点:每张独立打开处理保存,不要全部加载到内存。
问:几何操作会不会破坏原图?
crop/resize/rotate/transpose 都返回新对象,不修改原图(thumbnail 是唯一原地修改的)。这也是"先 copy 再折腾"习惯的来源——虽然多数操作本来就安全。
把几何操作组合成一个"九宫格切图"工具——微信朋友圈发九张图是高频需求:
from PIL import Image def nine_grid(img, gap=6, bg="white"): """把一张图切成 3x3 九宫格,每格带白边(朋友圈拼图风)""" w, h = img.size # 取中心方形区域 side = min(w, h) left = (w - side) // 2 top = (h - side) // 2 square = img.crop((left, top, left + side, top + side)) # 切成 9 块 piece = side // 3 pieces = [] for r in range(3): for c in range(3): box = (c * piece, r * piece, (c+1) * piece, (r+1) * piece) pieces.append(square.crop(box)) # 每块加白边并保存 canvas_size = piece + gap * 2 for i, p in enumerate(pieces): canvas = Image.new("RGB", (canvas_size, canvas_size), bg) canvas.paste(p, (gap, gap)) canvas.save(f"grid_{i+1}.jpg", quality=92) img = Image.open("photo.jpg") nine_grid(img) print("已生成 9 张朋友圈图")
九宫格的要点:先居中裁方形(避免变形)、再等分切块(整数除法保证整齐)、最后加白边(拼图留白)。这个工具完美展示了"裁剪 + 组合"的实战价值——一个需求,把 crop、resize、paste 全练了一遍。
几何操作最容易错的是坐标。给一个"坐标心法"总结:
坐标本质:图像左上角是 (0,0),x 向右、y 向下 crop 的 box:left, top 是起点(含),right, bottom 是终点(不含) → 宽 = right - left,高 = bottom - top 中心化公式:让对象居中 = (画布宽 - 对象宽) // 2 → x = (W - w) // 2,y = (H - h) // 2 等比缩放:保持比例 = 新宽/新高 = 原宽/原高 → 按宽度缩放: new_h = int(h * new_w / w) 对称裁剪:左右各裁 (w - target_w) // 2 → left = (w - target_w) // 2
心法要点:一切坐标问题都能拆成"先算尺寸、再算偏移、最后定边界"。画一张草图(原图矩形 + 目标区域)永远是第一步——坐标题"画出来"就解了一半。
几何操作对质量的影响,值得建立系统认知:
| 操作 | 质量影响 | 缓解手段 |
|---|---|---|
| crop | 无损失 | 只取区域 |
| resize 缩小 | 信息损失 | BILINEAR 足够 |
| resize 放大 | 插值失真 | LANCZOS/BICUBIC |
| rotate 90° 倍 | 无损失 | 用 transpose |
| rotate 任意角 | 重采样损失 | BICUBIC + 抗锯齿 |
| transpose | 无损失 | 纯搬移 |
质量管理的核心原则:
实践应用:做头像时,先 crop 出正方形区域(无损),再 resize 到目标尺寸(一次有损)——比"直接 resize 再裁"质量好得多。
把几何操作与绘图结合,做一个"批量图片水印排版"工具:
from PIL import Image, ImageDraw, ImageFont def watermark_layout(img, text="© MySite", position="br", size_ratio=0.05): """水印排版:位置可控、大小随图自适应""" img = img.convert("RGBA") w, h = img.size font_size = max(12, int(min(w, h) * size_ratio)) font = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", font_size) overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] margin = font_size pos = { "tl": (margin, margin), "tr": (w - tw - margin, margin), "bl": (margin, h - th - margin), "br": (w - tw - margin, h - th - margin), } draw.text(pos[position], text, fill=(255, 255, 255, 160), font=font) return Image.alpha_composite(img, overlay).convert("RGB") img = Image.open("photo.jpg") watermark_layout(img, "© 2026 MySite", "br").save("wm.jpg")
水印排版的通用性:位置(四角可选)、大小(随图自适应)、透明度(RGBA)三个参数可控——理解"相对定位"(用 margin 而非绝对值),排版就能自适应任意尺寸图片。这是批量加水印的核心技巧。
几何操作出错时,用这张排查表快速定位:
| 症状 | 可能原因 | 解决 |
|---|---|---|
| 裁剪后图是黑的 | 坐标越界/写反 | 检查 box 顺序与范围 |
| 旋转后尺寸不对 | 未用 expand | rotate 加 expand=True |
| 缩放后变形 | resize 非等比 | 用 fit/thumbnail |
| 翻转方向反了 | 混淆 FLIP 方向 | 查 transpose 文档 |
| 原图被改了 | 用了原地修改方法 | thumbnail 前先 copy |
排查思路:先确认"操作是否符合预期"(打印 size/mode 对比),再检查"参数语义"(方向、边界、比例)。几何操作的问题,90% 出在坐标与参数语义——画个草图、打印尺寸,问题就显形了。
把所有几何能力组合成一个"缩略图工厂",适应各种尺寸需求:
from PIL import Image, ImageOps def thumbnail_factory(img, target, mode="fit"): """缩略图工厂:多种模式输出""" w, h = target if mode == "fit": # 等比填满(裁边) return ImageOps.fit(img, target) elif mode == "contain": # 等比完整(留白) result = ImageOps.contain(img, target) # 铺到白底画布上 canvas = Image.new("RGB", target, "white") canvas.paste(result, ((w-result.width)//2, (h-result.height)//2)) return canvas elif mode == "stretch": # 拉伸填满(变形) return img.resize(target) elif mode == "blur": # 模糊填充 bg = ImageOps.fit(img.filter(ImageFilter.GaussianBlur(20)), target) fg = ImageOps.contain(img, target) bg.paste(fg, ((w-fg.width)//2, (h-fg.height)//2), mask=fg) return bg from PIL import ImageFilter img = Image.open("photo.jpg") for mode in ["fit", "contain", "stretch", "blur"]: thumbnail_factory(img, (300, 300), mode).save(f"factory_{mode}.jpg")
缩略图工厂的价值:四种模式覆盖了 Web 缩略图的全部常见需求(头像用 fit、预览用 contain、相册用 stretch、封面用 blur)——**"一个工厂,多种模式"**是工具化的经典设计。理解每种模式的取舍(裁边/留白/变形/填充),就能按业务选对模式。
基本操作都熟了,但"为什么有的图处理出来颜色怪怪的"?下一节补上底层认知——图像模式与像素原理。