1.2 图像的打开、保存与显示


1.2 图像的打开、保存与显示

本节摘要:Image.open、save、show 是 Pillow 读写显示的三板斧。本节详解 Image.open 的惰性加载特性、save 的格式转换与参数控制、show 的跨平台机制,并给出读取图片信息的完整方法,让你三分钟搞定"打开一张图并了解它"。

本节目标

阅读完本节,你应当能够:

  1. 用 Image.open 打开任意常见格式图片
  2. 用 save 保存为不同格式并控制质量
  3. 用 show 在本地显示图片
  4. 读取图片的尺寸、模式、格式等信息
  5. 理解 open 的惰性加载机制及其影响

问题与直觉:图像文件到内存的桥

一张 JPG 图片在磁盘上是压缩后的文件,Pillow 要做的是把它"解码"成内存里可操作的数据结构。Image.open() 就是这座桥——它返回一个 Image 对象,之后所有操作都围绕这个对象展开。

先看最基础的读取信息:

from PIL import Image img = Image.open("photo.jpg") print(img.size) # 尺寸 (宽, 高),如 (1920, 1080) print(img.mode) # 模式,如 RGB print(img.format) # 格式,如 JPEG

这三行就能告诉你一张图的基本身份。这也是排查"图片怎么不对"时的第一步——先看格式、尺寸、模式,再谈处理

核心原理:三个核心方法

1. Image.open——惰性加载

Image.open() 有一个重要特性:它是惰性加载的。也就是说,调用 open 时并不把整个图片读进内存,只读取文件头信息(尺寸、模式等)。真正的像素数据要等你实际访问(比如 img.load() 或执行操作)时才加载。

img = Image.open("big.jpg") # 此刻只读了头信息,很快 img.load() # 此刻才真正把像素读进内存

💡 关键直觉:惰性加载让"打开一个超大图片"变得几乎瞬时——Pillow 先探明情况,需要时再全量加载。但这个特性也带来一个坑:open 之后没有真正加载前,文件不能随意移动或删除(文件句柄可能还开着)。

2. save——写盘与格式转换

# 基本保存 img.save("output.png") # 格式转换(按扩展名自动识别) img.save("output.jpg") # 控制 JPEG 质量(1-100,越大质量越高文件越大) img.save("output.jpg", quality=85) # 控制 PNG 压缩级别(0-9,越大压缩率越高越慢) img.save("output.png", compress_level=6)

save 的格式由文件扩展名决定,这就是"格式转换"如此简单的原因。注意:转换格式可能丢失信息——比如 RGBA 的 PNG 转成 JPG,透明通道会被丢弃(JPG 不支持透明)。

3. show——本地显示

img.show()

show 会调用系统默认图片查看器打开图片。它是调试利器,但注意:show 是阻塞还是非阻塞取决于系统,且多张图连续 show 可能叠加窗口。生产环境建议直接 save 后用浏览器/脚本查看。

工程实践要点:常见操作场景

场景 做法 注意
读取图片信息 size/mode/format 三属性 排查问题的第一步
格式转换 save 换扩展名 JPG 丢透明通道
批量转码 循环 save 用 Path 管理文件名
控制文件大小 quality/compress_level 质量与体积的平衡
快速预览 show 适合本地调试

⚠️ 常见坑:open 后忘了处理文件句柄。在脚本里打开大量图片时,如果不主动关闭,可能耗尽文件描述符。推荐用上下文管理器:

from PIL import Image with Image.open("photo.jpg") as img: # 处理 img data = img.copy() # 需要长期使用就 copy 一份 # 退出 with 后文件句柄自动释放

动手实验:完整读写循环

from PIL import Image # 打开 with Image.open("photo.jpg") as img: print(f"原始: {img.format} {img.size} {img.mode}") # 生成缩略图(不会变形) img.thumbnail((300, 300)) print(f"缩略: {img.size}") # 保存为 PNG img.save("thumb.png") # 验证重新打开 back = Image.open("thumb.png") print(f"回读: {back.format} {back.size}")

运行后,你会看到尺寸从原图缩到 300 以内、格式变成 PNG、回读成功。这个循环就是日常图像处理的最小工作流——打开→处理→保存→验证

格式转换的细节与陷阱

格式与模式的关系

转换格式时,格式决定了"容器",模式决定了"内容结构"。两者要匹配才能保存:

目标格式 支持的常见模式 注意事项
JPEG RGB、L、CMYK 不支持透明、不支持 RGBA
PNG RGB、RGBA、L、P 支持透明,无损
GIF P(调色板) 只有 256 色
BMP RGB、L、P 无压缩,体积大
WebP RGB、RGBA、L 支持透明,体积小
TIFF 多种 可多帧、可无损

实操建议:不确定能不能存,先转成目标格式最常用的模式。比如"任何图存 JPEG"统一 convert("RGB"),"存 GIF"统一 convert("P")

保存路径与文件名

# 用 pathlib 管理路径更稳(避免字符串拼接坑) from pathlib import Path src = Path("photos") / "photo.jpg" out = Path("output") / (src.stem + "_thumb.png") # stem 是去扩展名的文件名 img.save(out)

⚠️ 常见坑:保存到不存在的目录会报错。save 不会自动创建目录——os.makedirs("output", exist_ok=True) 先建目录再保存。

批量场景的打开策略

实际项目中常要处理大量图片,打开方式直接影响性能:

逐个打开、用完即关

from PIL import Image import os for fname in os.listdir("photos"): if fname.lower().endswith((".jpg", ".png")): with Image.open(os.path.join("photos", fname)) as img: # 只做轻量操作(读属性、缩略图) info = (img.format, img.size, img.mode) # 走出 with,文件已释放 print(fname, info)

需要保留处理结果就 copy

with Image.open("photo.jpg") as img: result = img.copy() # 复制一份,with 退出后 result 仍可用 result.save("copy.png")

💡 关键直觉:open 是"轻量入口",copy 是"安全出口"。只读属性用 with+open,要长期持有对象用 copy,两者配合就不会踩"文件被占用""对象失效"的坑。

显示相关的平台差异

img.show() 在不同系统的表现差异较大,值得提前了解:

平台 show 的行为 备注
Windows 打开系统默认查看器 通常阻塞等待
macOS 打开预览 通过 open 命令
Linux 打开默认看图软件 依赖桌面环境
服务器 可能无反应 无图形界面

服务器/CI 环境建议不要用 show——没有图形界面时 show 可能报错或挂起。用 save 到文件再验证,或输出 Base64 编码检查。

从 Base64 读写图片

Web 开发中经常遇到 Base64 格式的图片(前端直传、JSON 接口),Pillow 配合 io 模块轻松处理:

from PIL import Image import io, base64 # Base64 → Image def b64_to_image(b64_str): data = base64.b64decode(b64_str) return Image.open(io.BytesIO(data)) # Image → Base64 def image_to_b64(img, format="JPEG", quality=85): buf = io.BytesIO() img.convert("RGB").save(buf, format=format, quality=quality) return base64.b64encode(buf.getvalue()).decode() # 示例:读图→转 Base64→还原 img = Image.open("photo.jpg") b64 = image_to_b64(img) print(f"Base64 长度: {len(b64)} 字符") back = b64_to_image(b64) print(f"还原: {back.size} {back.mode}")

BytesIO + Base64 是 Web 图片接口的标配——前端传 Base64、后端转图片处理、再回传 Base64。理解了 io.BytesIO 的用法,这条链路就通了。

常见错误排查表

打开保存阶段的高频报错,用这张表快速定位:

报错信息 原因 解决
FileNotFoundError 路径不存在 检查相对/绝对路径
OSError: cannot identify image file 不是图片/伪装文件 检查文件真实内容
SyntaxError: Non-UTF-8 文件名含特殊字符 用 pathlib 处理
UnidentifiedImageError 文件损坏 重新获取文件
PermissionError 文件被占用/只读 关闭占用进程,检查权限

⚠️ 常见坑:中文路径在某些旧版本有编码问题。用 pathlib.Path 或确保文件路径为 UTF-8 编码可避免大部分问题。Pillow 新版本对中文路径支持良好。

动手演练:批量格式转换器

把 1.2 的知识做成一个实用的"格式批量转换器":

from PIL import Image from pathlib import Path import os def convert_folder(src_dir, dst_dir, target_fmt="webp", quality=85): """批量转换文件夹内所有图片的格式""" src_path = Path(src_dir) dst_path = Path(dst_dir) dst_path.mkdir(exist_ok=True) supported = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"} count = 0 for f in src_path.iterdir(): if f.suffix.lower() not in supported: continue with Image.open(f) as img: # 按目标格式处理模式 if target_fmt in ("jpg", "jpeg", "webp"): img = img.convert("RGB") # 去透明 out_file = dst_path / f"{f.stem}.{target_fmt}" img.save(out_file, quality=quality) count += 1 print(f"转换: {f.name} → {out_file.name}") print(f"共转换 {count} 张") convert_folder("photos", "converted", "webp")

转换器的健壮点:扩展名白名单(防处理非图片文件)、目标格式的模式适配(JPG/WebP 转 RGB)、pathlib 路径管理。这个工具稍加扩展(加水印、加缩放)就是第四章批量处理的雏形——"先做工具,再进化成流水线"

读写环节的进阶话题

打开保存是每次处理的"入口与出口",几个进阶话题提前了解:

流式处理大文件:用 Image.open + 分块 crop 处理超大图,避免整图载入内存(详见 3.5 金字塔)。

内存与文件句柄:open 后不处理就 close(或 with),防止文件句柄耗尽——批量场景尤其重要。

保存参数的组合拳:quality + optimize + progressive 可以同时使用,兼顾质量、体积、加载体验(详见 3.4 格式进阶)。

验证闭环习惯每次处理完都 reopen 验证(格式、尺寸、模式是否符合预期),是防止"静默错误"的最好习惯——处理结果和预期不符时,第一时间发现。

动手演练:图片批量重命名与转换

把本节知识组合成一个小工具——"批量重命名 + 格式转换":

from PIL import Image from pathlib import Path import os def rename_and_convert(folder, prefix="img", target_fmt="webp", quality=85): """批量重命名(编号)并转换格式""" src = Path(folder) dst = src / "converted" dst.mkdir(exist_ok=True) supported = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"} files = sorted(f for f in src.iterdir() if f.suffix.lower() in supported) for i, f in enumerate(files, 1): with Image.open(f) as img: if target_fmt in ("jpg", "webp"): img = img.convert("RGB") out = dst / f"{prefix}_{i:03d}.{target_fmt}" img.save(out, quality=quality) print(f"{f.name} → {out.name}") print(f"完成 {len(files)} 张") rename_and_convert("photos", prefix="photo", target_fmt="webp")

这个工具的价值:命名规范化(编号)+ 格式统一(WebP)是素材管理的常见需求。pathlib 的 iterdir + 扩展名过滤 + 编号命名是这类工具的通用骨架,稍加修改就能适配"加水印、加缩放"等更多处理。

读写相关的常见误区

最后总结几个读写环节的高频误区:

误区 正确认知
"open 就是读取完成" open 是惰性的,像素未加载
"save 会自动建目录" 不会,目录要先存在
"扩展名决定一切" 内容才是关键,扩展名只是约定
"转格式无损" 转 JPG 有损、丢透明
"处理完必须手动 close" with 自动管理
"显示只能用 show" 服务器环境用 save/Base64

误区根源:把"图像文件"当成"普通文件"理解。图像文件有"解码"这一步——open 是打开文件、load 才是解码内容、save 是编码写出。理解这三步的分离,读写环节就不会出错了。

动手演练:图片处理"入口工具"

把 1.2 的能力封装成一个"图片入口工具",统一处理各种来源的图片:

from PIL import Image import io, os def load_image_from_any(source): """从任意来源加载图片:文件路径 / bytes / Image 对象""" if isinstance(source, Image.Image): return source.copy() if isinstance(source, (bytes, bytearray)): return Image.open(io.BytesIO(source)) if isinstance(source, str): if os.path.exists(source): return Image.open(source) raise FileNotFoundError(f"路径不存在: {source}") raise TypeError(f"不支持的来源类型: {type(source)}") def save_image_any(img, target): """保存到任意目标:路径 / BytesIO""" if isinstance(target, str): img.save(target) else: img.save(target) return target # 使用 img = load_image_from_any("photo.jpg") # 从路径 with open("photo.jpg", "rb") as f: img2 = load_image_from_any(f.read()) # 从 bytes img3 = load_image_from_any(img2) # 从 Image 对象

入口工具的价值:统一"图片从哪来"的接口,让上层处理逻辑不用关心来源。**"入口统一、处理统一、出口统一"**是图片处理系统设计的核心原则——数据来源再多,处理层只认一种形态。

实战:图片格式转换的完整对比

做一个"格式转换对比实验",用数据指导选型:

from PIL import Image import os def format_compare(img, formats, base="cmp"): """对比各格式的体积""" results = {} for fmt in formats: path = f"{base}.{fmt.lower()}" img.save(path, format=fmt.upper(), quality=85) results[fmt] = os.path.getsize(path) return results img = Image.open("photo.jpg").convert("RGB") results = format_compare(img, ["JPEG", "PNG", "WEBP", "BMP"]) # 打印对比 for fmt, size in results.items(): print(f"{fmt:6s}: {size//1024} KB") # 找最小的 best = min(results, key=results.get) print(f"最小体积: {best} ({results[best]//1024} KB)")

这个实验的意义:用你自己的图片实测,比看任何"格式对比文章"都可靠——数据因图而异(照片、截图、Logo 的最优格式不同)。理解"先实测再选型",是工程决策的基本功。

读写性能与内存细节

打开保存环节的性能与内存,直接影响大批量任务的效率:

import time from PIL import Image import io # 读取方式的性能差异 def bench_read(path, iterations=30): # 方式 A:直接读文件 t0 = time.time() for _ in range(iterations): with Image.open(path) as img: img.load() t_file = (time.time() - t0) / iterations # 方式 B:先读 bytes 再解 with open(path, "rb") as f: data = f.read() t0 = time.time() for _ in range(iterations): img = Image.open(io.BytesIO(data)) img.load() t_bytes = (time.time() - t0) / iterations print(f"直接读文件: {t_file*1000:.1f}ms/次") print(f"先读 bytes: {t_bytes*1000:.1f}ms/次") return t_file, t_bytes bench_read("photo.jpg")

读取性能要点

  1. 小图差异不大,大图/批量才有意义
  2. bytes 方式适合"同一数据多次处理"(Web 服务中一次读入、多次用)
  3. load 是解码的开关——不 load 就不花解码时间(只读属性时)

内存细节open 本身几乎不占内存(惰性),load 后才占(像素数据)。"只看属性不处理"时可以不 load——比如批量检查尺寸,省内存省时间。

动手实验:图片信息速查器

把读写能力做成一个"图片信息速查器",快速了解任意图片:

from PIL import Image import os def image_info(path): """输出图片的完整信息""" with Image.open(path) as img: w, h = img.size mode = img.format size_kb = os.path.getsize(path) / 1024 bytes_per_pixel = {"1": 0.125, "L": 1, "P": 1, "RGB": 3, "RGBA": 4, "CMYK": 4}.get(mode, 1) mem_mb = w * h * bytes_per_pixel / (1024 * 1024) dpi = img.info.get("dpi", "无") return { "文件": os.path.basename(path), "尺寸": f"{w}x{h}", "模式": mode, "文件大小": f"{size_kb:.0f} KB", "内存估算": f"{mem_mb:.1f} MB", "DPI": dpi, } # 查看多张图 for f in ["photo.jpg", "logo.png", "icon.ico"]: if os.path.exists(f): info = image_info(f) print(" | ".join(f"{k}:{v}" for k, v in info.items()))

信息速查器的价值:一次输出"尺寸/模式/格式/文件大小/内存估算"——处理前的"体检"。批量场景先跑一遍,能提前发现"这张图多大、会不会爆内存";Web 场景判断"该不该压缩"。**"先了解、再处理"**是图像工程的基本习惯。

一节小结

  • open 惰性加载:先读头信息,访问像素时才全量加载
  • save 按扩展名定格式:换扩展名即转格式
  • JPG 无透明通道:RGBA 转 JPG 会丢透明
  • 质量参数:JPG 用 quality,PNG 用 compress_level
  • 上下文管理器:with open 自动释放文件句柄
  • 格式模式匹配:存前先转目标格式常用模式
  • 目录要预先创建:save 不自动建目录
  • Base64 读写:Web 图片接口的标配链路
  • 转换器工具:白名单+模式适配+pathlib
  • 重命名工具:编号命名+格式统一
  • 报错排查表:五类高频错误快速定位
  • 验证闭环:处理完 reopen 验证,防静默错误
  • 入口工具:统一来源接口,处理层只认一种形态
  • 格式实测:数据因图而异,先测再选型
  • 读取性能:bytes 复用、load 是解码开关
  • 信息速查器:处理前体检,尺寸/模式/内存
  • 三步分离:open/load/save 各管一段
  • 平台差异:服务器环境别用 show
  • 读写验证闭环:open→处理→save→reopen,是标准工作流

打开了、保存了,但"Image 对象"本身还没深入。下一节我们解剖 Image 对象——它的属性、方法与常见操作。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U