
第017期 | 图像处理算法入门系列 | 2026-08-26(周三)
关键词:cv2.imwrite、JPEG quality、PNG compression、WEBP、批量格式转换、CLI pipeline
一、引言
上一期我们把多张图拼成一张大图,今天解决一个更工程化的问题:如何把一堆图像高效地转换成另一种格式/尺寸。这是博客图床、电商商品图、相册备份、数据集预处理、网站图片发布都会遇到的刚需。
本期要回答的核心问题:
代码 0 外部依赖,直接 python code/017_batch_pipeline.py 跑通。
二、核心概念与原理
2.1 图像格式转换的本质
图像格式 = 像素矩阵 + 编码器 + 压缩策略
BMP 几乎无压缩 → 体积最大,解码最快
PNG 无损压缩(基于 DEFLATE)→ 保留全部信息,适合截图/线稿
JPEG 有损压缩(基于 DCT)→ 适合照片,质量参数 q 决定压缩率
WEBP 有损/无损双模 → 互联网发布首选,相同质量通常比 JPEG 小 25%-35%
2.2 JPEG 质量参数 q
q 范围 0-100
q 越小 → 丢弃高频信息越多 → 块效应、色彩断层越明显 → 体积越小
q 越大 → 接近原图 → 体积越大
OpenCV 写法:
cv2.imwrite('out.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 85])
经验区间:
| 网页缩略图 | 60-75 | 肉眼可接受,体积最小 |
| 常规展示 | 80-90 | 画质与体积平衡 |
| 存档/印刷 | 95 | 几乎无压缩痕迹,但体积接近 PNG |
2.3 PNG 压缩级别
OpenCV 写法:
cv2.imwrite('out.png', img, [cv2.IMWRITE_PNG_COMPRESSION, 3])
compression 范围 0-9
0 → 不压缩(文件甚至比 BMP 还大)
3 → 速度与体积的甜点
9 → 最大压缩(解码稍慢,体积收益递减)
PNG 是无损的,压缩级别只影响压缩率,不影响画质。
2.4 WEBP 的两种模式
有损 WEBP:
cv2.imwrite('out.webp', img, [cv2.IMWRITE_WEBP_QUALITY, 85])
无损 WEBP(libwebp 约定 quality >= 101 即无损):
cv2.imwrite('out.webp', img, [cv2.IMWRITE_WEBP_QUALITY, 101])
| 有损 WEBP | 体积通常 < JPEG,支持透明 | 网页/移动端图片 |
| 无损 WEBP | 体积 ≈ PNG 级,但通常小 10%-20% | 需要保留透明+无损 |
2.5 OpenCV imwrite 参数表
| IMWRITE_JPEG_QUALITY | JPEG 质量 | 0-100,默认 95 |
| IMWRITE_PNG_COMPRESSION | PNG 压缩级别 | 0-9,默认 3 |
| IMWRITE_WEBP_QUALITY | WEBP 质量 | 0-100 有损,>=101 无损 |
2.6 批量处理 pipeline 设计要点
一个好的批量转换脚本应该具备:
1. 输入目录扫描(过滤图片后缀)
2. 输出目录自动创建
3. 统一编码参数(格式、质量)
4. 可选的等比缩放(max_side)
5. 单张失败不中断整批
6. 实时进度条 + 汇总报告
三、代码实践
3.1 测试图生成(0 外部数据)
import cv2
import numpy as np
def make_photo_like(w=640, h=480):
"""生成一张模拟照片:渐变天空 + 细节纹理 + 纯色块 + 文字"""
# 垂直渐变天空(JPEG 压缩敏感)
sky = np.linspace(180, 40, h, dtype=np.float32).reshape(h, 1)
img = np.zeros((h, w, 3), dtype=np.float32)
img[:, :, 0] = sky
img[:, :, 1] = sky * 0.8
img[:, :, 2] = sky * 1.2
# 细节纹理区
rng = np.random.default_rng(42)
tile = 16
for y in range(0, h // 2, tile):
for x in range(w // 2, w, tile):
if ((x // tile + y // tile) % 2) == 0:
img[y:y + tile, x:x + tile] = [230, 120, 40]
noise = rng.integers(–25, 25, (h // 2, w // 2, 3))
img[:h // 2, w // 2:] = np.clip(img[:h // 2, w // 2:] + noise, 0, 255)
# 纯色块 + 文字
img[h // 2:, :w // 2] = [90, 160, 60]
img = np.clip(img, 0, 255).astype(np.uint8)
cv2.putText(img, 'FORMAT 017', (40, h – 60),
cv2.FONT_HERSHEY_SIMPLEX, 1.4, (255, 255, 255), 4)
return img
3.2 JPEG 质量参数横评
import os
def demo_jpeg_quality(img, output_dir):
qualities = [10, 30, 50, 70, 90, 95]
print('质量 体积(KB) 压缩比')
for q in qualities:
tmp = os.path.join(output_dir, f'_q{q}.jpg')
cv2.imwrite(tmp, img, [cv2.IMWRITE_JPEG_QUALITY, q])
kb = os.path.getsize(tmp) / 1024
ratio = img.nbytes / 1024 / kb
print(f'q={q:<3} {kb:8.1f} {ratio:6.1f}x')
3.3 格式体积对比
def demo_format_compare(img, output_dir):
tmp = os.path.join(output_dir, '_tmp')
records = [('BMP 原始', os.path.getsize(tmp + '.bmp') / 1024)]
# PNG 不同压缩级
for lvl in (0, 3, 9):
cv2.imwrite(tmp + '.png', img, [cv2.IMWRITE_PNG_COMPRESSION, lvl])
records.append((f'PNG 压缩级={lvl}', os.path.getsize(tmp + '.png') / 1024))
# JPEG / WEBP 不同质量
for q in (50, 90):
cv2.imwrite(tmp + '.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, q])
records.append((f'JPEG q={q}', os.path.getsize(tmp + '.jpg') / 1024))
for q in (50, 90):
cv2.imwrite(tmp + '.webp', img, [cv2.IMWRITE_WEBP_QUALITY, q])
records.append((f'WEBP q={q}', os.path.getsize(tmp + '.webp') / 1024))
# 无损 WEBP(OpenCV 5.x 已无 IMWRITE_WEBP_LOSSLESS,用 quality=101)
cv2.imwrite(tmp + '_l.webp', img, [cv2.IMWRITE_WEBP_QUALITY, 101])
records.append(('WEBP 无损', os.path.getsize(tmp + '_l.webp') / 1024))
for name, kb in records:
print(f'{name:<14} {kb:8.1f} KB')
3.4 可复用的批量转换函数
import time
def convert_batch(src_dir, dst_dir, fmt='jpg', quality=85, max_side=None):
"""批量格式转换 + 可选缩放 + 统一输出。"""
fmt = fmt.lower().lstrip('.')
encode_params = {
'jpg': [cv2.IMWRITE_JPEG_QUALITY, quality],
'webp': [cv2.IMWRITE_WEBP_QUALITY, quality],
'png': [cv2.IMWRITE_PNG_COMPRESSION, 3],
}[fmt]
valid_ext = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'}
os.makedirs(dst_dir, exist_ok=True)
files = [f for f in sorted(os.listdir(src_dir))
if os.path.splitext(f)[1].lower() in valid_ext]
report = {'ok': 0, 'fail': 0, 'src_kb': 0.0, 'dst_kb': 0.0, 't0': time.time()}
for i, fname in enumerate(files, 1):
src_path = os.path.join(src_dir, fname)
try:
img = cv2.imread(src_path, cv2.IMREAD_COLOR)
if img is None:
raise IOError(f'无法解码: {fname}')
# 等比缩放:只缩小,不放大
if max_side:
h, w = img.shape[:2]
scale = max_side / max(h, w)
if scale < 1.0:
img = cv2.resize(img, (int(w * scale), int(h * scale)),
interpolation=cv2.INTER_AREA)
stem = os.path.splitext(fname)[0]
dst_path = os.path.join(dst_dir, f'web_{stem}.{fmt}')
cv2.imwrite(dst_path, img, encode_params)
report['ok'] += 1
report['src_kb'] += os.path.getsize(src_path) / 1024
report['dst_kb'] += os.path.getsize(dst_path) / 1024
except Exception as e:
report['fail'] += 1
print(f' [skip] {fname}: {e}')
continue
done = int(i / len(files) * 30)
print(f'\\r[{"#" * done}{"." * (30 – done)}] {i}/{len(files)}', end='')
print()
report['time_s'] = time.time() – report['t0']
return report
3.5 CLI 入口
import argparse
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='图像格式转换与批量处理')
parser.add_argument('–batch-src', help='源目录')
parser.add_argument('–batch-dst', help='输出目录')
parser.add_argument('–fmt', default='jpg', help='目标格式')
parser.add_argument('–quality', type=int, default=85)
parser.add_argument('–max-side', type=int, default=None)
args = parser.parse_args()
# 没有自定义目录时跑内置 demo
if args.batch_src:
rep = convert_batch(args.batch_src, args.batch_dst or args.batch_src + '_out',
fmt=args.fmt, quality=args.quality, max_side=args.max_side)
else:
# 自动生成 8 张测试图并跑 pipeline
rep = demo_batch_pipeline()
print(f"成功 {rep['ok']} / 失败 {rep['fail']} | "
f"总体积 {rep['src_kb']:.0f}KB → {rep['dst_kb']:.0f}KB "
f"(节省 {(1 – rep['dst_kb'] / max(rep['src_kb'], 1)) * 100:.1f}%) | "
f"耗时 {rep['time_s']:.2f}s")
完整代码已整理到:code/017_batch_pipeline.py
四、运行结果与可视化
执行 python code/017_batch_pipeline.py,关键输出如下:
[1/3] JPEG 质量参数横评 …
质量 体积(KB) 压缩比
q=10 6.5 137.8x
q=30 10.6 85.2x
q=50 14.6 61.5x
q=70 21.6 41.7x
q=90 44.6 20.2x
q=95 65.9 13.7x
[2/3] BMP/PNG/JPEG/WEBP 体积对比 …
BMP 原始 900.1 KB
PNG 压缩级=0 902.0 KB
PNG 压缩级=3 197.2 KB
PNG 压缩级=9 191.7 KB
JPEG q=50 14.6 KB
JPEG q=90 44.6 KB
WEBP q=50 9.4 KB
WEBP q=90 34.8 KB
WEBP 无损 176.8 KB
[3/3] 批量 pipeline 实战(8 张模拟原图 → web 发布规格)…
成功 8 / 失败 0 | 总体积 887KB → 613KB (节省 31.0%) | 耗时 0.46s
生成的可视化图(发布前请上传至 CSDN 图床并替换链接):
JPEG 质量参数对比

格式体积横评

批量 pipeline 效果

五、常见问题 FAQ
Q1:PNG 压缩级调到 9,为什么体积和压缩级 3 差不多?
A:PNG 压缩是无损的,压缩级别只影响编码强度,不改变熵。对于简单图案(如渐变、纯色块),DEFLATE 在较低级别已经把冗余去得差不多了,继续调高收益很小。
Q2:JPEG q=100 就是无损吗?
A:不是。JPEG 就算 q=100 也是有损 DCT 量化,只是量化步长很小。需要真正无损请用 PNG 或 WEBP 无损。
Q3:为什么批量转换时单张失败不能中断?
A:工程场景下原图目录里可能混有损坏文件、非图像文件。try/except 跳过单张,保证大批量任务能跑完,再回头处理异常文件。
Q4:WEBP 兼容性如何?
A:现代浏览器(Chrome/Firefox/Safari/Edge)均已支持。老旧 Windows 图片查看器可能无法直接预览,发布前需确认目标平台。
六、产品推荐
| 移动端 | 格式工厂 App | 图片/视频格式转换 | 手机快速转格式、压缩发圈 |
| 移动端 | Snapseed | 修图 + 导出控制 | 移动端图片编辑、按需求导出 JPG |
| 移动端 | 迅捷图片转换器 | 格式/压缩/压缩包 | 批量手机图片转 WEBP/PDF |
| 专业级 | XnConvert | 批量图像处理神器 | 数万张图片批量转格式、加水印、调尺寸 |
| 专业级 | ImageMagick | 命令行图像工具 | 服务器端批处理、脚本化 |
| 专业级 | FFmpeg | 多媒体处理 | 视频抽帧、图集生成 |
| 专业级 | Adobe Bridge | 图片管理 + 批量导出 | 摄影师/设计师工作流 |
推荐组合:
- 个人日常:Snapseed 修图 → 格式工厂 App 压缩 → 发朋友圈/小红书。
- 批量工作流:XnConvert / ImageMagick / 本期 convert_batch() 脚本。
- 网站发布:原图 PNG → 无损 WEBP 或有损 WEBP,体积最小。
七、课后练习
下期预告
第018期:简易图像浏览器开发
我们将用 OpenCV + Tkinter 做一个能读取、显示、缩放、切换、展示图像信息的迷你图片浏览器,把前面 8 期的基础操作串成一个可用的小工具。

![【题解】[COCI 2025/2026 #6] 滑雪 / Skijanje(李超树 0 基础友好喵)-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260826083930-6a8ea642697bc-220x25.png)
