欢迎光临
我们一直在努力

图像处理入门017 | 图像格式转换与批量处理 pipeline

在这里插入图片描述

第017期 | 图像处理算法入门系列 | 2026-08-26(周三)
关键词:cv2.imwrite、JPEG quality、PNG compression、WEBP、批量格式转换、CLI pipeline


一、引言

上一期我们把多张图拼成一张大图,今天解决一个更工程化的问题:如何把一堆图像高效地转换成另一种格式/尺寸。这是博客图床、电商商品图、相册备份、数据集预处理、网站图片发布都会遇到的刚需。

本期要回答的核心问题:

  • JPEG 的 quality 参数到底影响什么?q=50 和 q=90 差多少 KB?
  • PNG 的 compression 参数调高调低,为什么有时体积几乎不变?
  • WEBP 凭什么比 JPEG 还小?什么场景应该用 WEBP 无损?
  • 如何写一个可复用的批量转换脚本,支持格式、质量、最长边、失败跳过、进度条?
  • 代码 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])

    经验区间:

    场景推荐 q说明
    网页缩略图 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 质量参数对比
    ![017_jpeg_quality_compare.png](code/output/017_jpeg_quality_compare.png)

    格式体积横评
    ![017_format_size_compare.png](code/output/017_format_size_compare.png)

    批量 pipeline 效果
    ![017_batch_pipeline_result.png](code/output/017_batch_pipeline_result.png)


    五、常见问题 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,体积最小。

    七、课后练习

  • 参数敏感性实验:把 convert_batch 的 quality 分别改成 60、75、90,观察 8 张图总体积与单张体积变化。
  • 格式对比实验:把目标格式改成 webp,对比相同质量下 jpg 与 webp 的总体积差异。
  • 异常鲁棒性:在源目录里放一张损坏的 .jpg 文件,验证 pipeline 是否会跳过并继续处理剩余图片。
  • 扩展功能:在 convert_batch 中增加"输出文件名前缀 + 序号"功能(如 web_0001.jpg)。

  • 下期预告

    第018期:简易图像浏览器开发

    我们将用 OpenCV + Tkinter 做一个能读取、显示、缩放、切换、展示图像信息的迷你图片浏览器,把前面 8 期的基础操作串成一个可用的小工具。

    赞(0)
    未经允许不得转载:171主机测评 » 图像处理入门017 | 图像格式转换与批量处理 pipeline
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址