You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pillow中像素缩放50%时图片KB大小未同比下降原因及调控方法

问题:能否将图片的KB级存储大小精准调控到目标值?
  • 核心疑问:此前回答中提到4种获取图片大小的方式,这些大小指标是否和像素总数存在关联?
  • 实测数据:采集到多组(os_stat_size, numpy_array_size, image_factor)数据如下:
    (7460,24300,1),(2549,6075,2),(1303,2700,3),(808,1518,4), (552,972,5)
    
    其中image_factor代表图片缩放因子,实测可见缩放比例和图片KB级存储大小不成线性比例。
  • 实验背景:当前使用可正常运行的图片缩放代码开展实验,目标是直接调控图片的KB级存储大小而非像素尺寸。最初实验假设位深固定、元数据不变的前提下,像素数量和图片大小呈线性对应关系,计划将图片整体存储大小降低50%。
  • 初步验证:最初使用jpeg格式实验未得到预期线性结果,猜测是格式特性导致,查阅资料看到如下描述:

    JPEG文件不存在GIF或PNG文件意义上的位深,生成JPEG数据所使用的变换算法会在解压时渲染连续的色谱。
    换用PNG格式重复实验后,依然得到非线性结果。

实验代码

依赖导入与辅助缩放函数(运行正常)

from PIL import Image
import numpy as np
import requests
from io import BytesIO
from os import stat
import os
import sys

def resize(image:Image, current_size:tuple, new_width_height:float)->Image:
    '''
    缩放并返回处理后的图片
    参数说明:
        image: PIL Image对象
        current_size: 原始图片尺寸,格式为(宽, 高)
        new_width_height: 缩放后尺寸,传入整数时会保持宽高比,将较长边缩放到该值后等比计算短边长度;传入元组时直接缩放到指定宽高
    '''
    fixed_size = True if isinstance(new_width_height, int) else False
    w, h = current_size

    if fixed_size:
        if h > w:
            fixed_height = new_width_height
            height_percent = (fixed_height / float(h))
            width_size = int((float(w) * float(height_percent)))
            image = image.resize((width_size, fixed_height), Image.Resampling.LANCZOS)
        else:
            fixed_width = new_width_height
            width_percent = (fixed_width / float(w))
            height_size = int((float(h) * float(width_percent)))
            image = image.resize((fixed_width, height_size), Image.Resampling.LANCZOS) 
    else:
        w, h = int(new_width_height[0]), int(new_width_height[1])
        image = image.resize((w,h), Image.Resampling.LANCZOS)

    return image

实验步骤

  1. 原始图片统计数据获取
url = "https://wallpaperaccess.com/full/508751.jpg"
buffer = BytesIO(requests.get(url).content)
size_buffer = buffer.getbuffer().nbytes//1024
image = Image.open(buffer)

size_uncompressed = len(image.tobytes()) // 1024

# 切换JPEG格式时取消下方两行注释
# image.save('./wallpaper.jpeg', format = 'jpeg', quality = 'keep', subsampling = False)
# size_os = os.stat('./wallpaper.jpeg').st_size // 1024
# PNG格式测试
image.save('./wallpaper.PNG', format = 'PNG', quality = 'keep', subsampling = False)
size_os = os.stat('./wallpaper.PNG').st_size // 1024
numpy_size = sys.getsizeof(np.array(image)) // 1024 # 与len(image.tobytes())结果一致

print(image.size, image.format, image.mode, str(size_buffer)+' KB (buffer.getbuffer().nbytes//1024)', str(size_os)+' KB (os.stat().st_size // 1024)', str(size_uncompressed)+' KB (len(image.tobytes()) // 1024)', str(numpy_size)+' KB (sys.getsizeof(np.array(image)) // 1024)')
  1. 缩放后图片统计数据获取
image = resize(image, image.size, max(image.size)//2)
# 切换JPEG格式时取消下方注释
# image.format = "JPEG" # jpeg格式使用quality='keep'参数时必须指定format属性
image.format = "PNG" 

size_uncompressed = len(image.tobytes()) // 1024

buffer = BytesIO()
# 切换JPEG格式时取消下方注释
# image.save(buffer, format="JPEG", format = 'jpeg', quality = 'keep', subsampling = False)
image.save(buffer, format="PNG")
size_buffer = buffer.getbuffer().nbytes//1024

# 切换JPEG格式时取消下方两行注释
# image.save('./resized_wallpaper.jpeg', format = 'jpeg', quality = 'keep', subsampling = False)
# size_os = os.stat('./resized_wallpaper.jpeg').st_size // 1024
# PNG格式测试
image.save('./resized_wallpaper.PNG', format = 'PNG')
size_os = os.stat('./resized_wallpaper.PNG').st_size // 1024

numpy_size = sys.getsizeof(np.array(image)) // 1024

print(image.size, image.format, image.mode, str(size_buffer)+' KB (buffer.getbuffer().nbytes//1024)', str(size_os)+' KB (os.stat().st_size // 1024)', str(size_uncompressed)+' KB (len(image.tobytes()) // 1024)',str(numpy_size)+' KB (sys.getsizeof(np.array(image)) // 1024)')
  • 最终疑问:实验测得所有大小指标均未随像素缩放呈线性变化,该现象是正常的,还是实验操作存在错误?

回答

这个现象完全正常,线性假设从根本上不成立,核心问题是混淆了未压缩原始像素数据大小和压缩后文件存储大小两个完全独立的概念。

  1. 只有未压缩的原始像素数据才和像素数呈严格线性关系:你测的numpy_array_size、len(image.tobytes())这两个值,本质是解压后每个像素按固定位深存储的原始大小,比如RGB模式每个像素占3字节,RGBA占4字节,这部分值确实严格等于像素数 * 每像素字节数,缩放后像素数降到对应比例,这部分值就会同步降到对应比例。你之前测的数据里numpy_array_size从24300降到6075、2700,刚好是1/4、1/9的比例,和缩放因子的平方完全对应,这部分本身就符合线性规律。
  2. 你测的os_stat_size、内存buffer里的文件大小,是经过压缩编码后的文件大小,和像素数没有固定线性关系:
    • 对JPEG这类有损压缩格式,压缩率和图片内容复杂度强相关:高频细节多的区域(比如纹理复杂的草地、密集文字)压缩率低,平滑渐变区域压缩率高。你用LANCZOS算法缩放图片时,本身会抹除一部分高频细节,小尺寸图的单位像素压缩率会比原图高很多,最终文件大小下降速度会远快于像素数下降速度。再加JPEG的质量参数、子采样设置也会极大影响最终大小,和像素数不存在固定比例。
    • 对PNG这类无损压缩格式,核心靠DEFLATE算法查找重复数据块实现压缩。缩放后的图片像素排列规律、相邻像素差值分布和原图完全不同,压缩率会发生不可预测的变化:如果缩放后出现大面积同色区域,压缩率会大幅升高;如果缩放后插值产生的噪点变多、相邻像素差异变大,压缩率反而会降低,最终大小和像素数同样没有线性关系。
  3. 直接调控图片到目标KB大小的通用方案:不要靠缩放像素凑大小,用二分法迭代调整编码参数即可:
    • 对JPEG:固定分辨率,在1-95的质量值区间做二分,每次编码后判断文件大小,直到输出大小落在目标值的误差允许范围内即可。
    • 对PNG:可以调整压缩级别(0-9)、是否开启隔行扫描、位深、调色板大小来控制输出大小,同样用二分法迭代就能逼近目标值。
      注意所有有损压缩格式都无法做到100%精准命中目标大小,一般控制在目标值±5%以内就属于合格结果。

内容的提问来源于stack exchange,提问作者Deshwal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 05:18:54