这是我在写博客时遇到的问题,网页加载图片的速度并不快,想要网页显示图片又不拖慢速度的话,就要限制图片的大小。我们平时为了追求高质量图片,对于自己一见钟情的图片,通常都会选择保存原图,而原图大小几乎都是以 Mb 为单位的,肯定是不能直接放到网页上的,否则就会加载非常非常非常久。于是我百度了一下“压缩图片”,出来的全是广告,又要下载这个那个的,还不如自己写个程序,于是就有了这篇文章。

不过,本篇只讨论函数接口的调用,并不打算详细研究其实现,也不深入探究各种图像压缩算法,也就是说,只讲使用,不讲原理。使用 python 的原因很简单:环境配置容易,代码简洁,有成熟的函数直接供我们使用。

环境准备(安装python包)

我们将会用到两个库,一个是 opencv,这个是著名的计算机视觉库了,另一个是 pillow,也就是 PIL(Python Image Library),这个也是经典的 python 图像处理库。执行下面两句命令行,利用 pip 工具安装两个工具库:

pip  install  opencv-python==3.4.17.61
pip  install  pillow

压缩分辨率

以 .jpg 图片为例,图片文件的大小由很多因素决定,其中比较重要的有两个,一个是图像的分辨率,另一个是图像的质量。

图像的分辨率决定了像素的总量,而图像的质量往往跟色彩有关。我们可以从这两方面去压缩图片的大小,下面先介绍如何改分辨率,先上代码:

import cv2

img = cv2.imread("img.jpg") # 读取原图
print(img.shape) # 查看原图分辨率
new_img = cv2.resize(img, (1080, 1920)) # 修改图片分辨率
print(new_img.shape) # 查看修改后的图片分辨率
cv2.imwrite("new.jpg", new_img) # 保存修改后的图片

上面的代码非常简单,只有三个步骤:读取原图→改分辨率→保存新图。python 中的 opencv 在使用时叫 cv2,所以在使用 opencv 的函数前,要先写一句 import cv2 导入 opencv 库。

imread 函数用于读取图片,传入的是图片文件名(后缀名记得写对),返回一个多维数组,其中存储了图片的像素值。需要注意的是,我们平时描述图片的分辨率习惯用行数×列数的形式,比如 1920×1080、1280×720 之类的都是行×列的形式,但是 opencv 读取图片后的多维数组是列数×行数的形式,所以,在调用 resize 函数来修改分辨率时,我们要小心行数和列数的位置,不要写错。resize 函数的第一个传入参数是原图,第二个参数是一个元组,以(列,行)的形式表示新图的分辨率,返回新图的多维数组。imwrite 函数用于写入图片,第一个参数是文件名,第二个参数是多维数组,其中保存了图片的像素值。

上面的代码执行完毕后,会把 img.jpg 修改成 1920×1080 的分辨率然后保存在 new.jpg 中。但是这显然有一个问题,因为 1920×1080 的图片的横纵比是 16:9,但是并不是所有的图片都是 16:9 ,如果每张图片都按这样的方法压缩的话,就会改变图片的横纵比,使图片变形。想要保持图片的横纵比的话,我们可以等比例压缩,使得新图的横纵比跟原图保持一致。

实现等比例压缩有两种方法,一是我们自己手动算出最终的分辨率,然后代入上面的代码,比如上面的 resize 函数调用改成 new_img = cv2.resize(img, (img.shape[0]*0.5, img.shape[1]*0.5)),这样就把图片的行数和列数都变成原来的一半。

前面所说的方法,都是以固定分辨率的形式进行传参,而 resize 函数有更直接的方式让我们按比例改变图片分辨率,我们将代码修改为下面的形式:

import cv2

img = cv2.imread("img.jpg") # 读取原图
print(img.shape) # 查看原图分辨率

rate = 0.5 # 修改分辨率的比例
new_img = cv2.resize(img, (0, 0), fx = rate, fy = rate) # 修改图片分辨率
print(new_img.shape) # 查看修改后的图片分辨率

cv2.imwrite("new.jpg", new_img) # 保存修改后的图片

可以看出,resize 函数允许我们用 fx 参数和 fy 参数来用百分比的形式修改图片的分辨率,这两个参数会分别乘上原图的行数和列数来得到新图的分辨率,当给 fxfy 赋值之后,前面的第二项参数就失效了,所以可以直接用 (0,0) 来传参。

压缩图片质量

jpg 图片是经典的图片文件格式,它是自带压缩的,而这种压缩的程度是可选的。当我们选择保存为 .jpg 图片时,即使分辨率相同,最终图片的大小也会因为 jpg 压缩程度的不同而不同。我们可以在调用 imwrite 时,有很多参数可以选,其中一项参数cv2.IMWRITE_JPEG_QUALITY就是用于调整 jpg 的压缩程度,从而影响最终图片质量和大小。我们可以像下面这样来试验一下:

import cv2

img = cv2.imread("img.jpg")

cv2.imwrite("new.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 80]) 

上面代码就是普通的读取图片和保存图片,并没有改变图片的分辨率,不过在 imwrite 中指明了 cv2.IMWRITE_JPEG_QUALITY 的参数是 80,这个参数的取值范围是 0 到 100,0 表示最差质量,压缩程度最高,图片文件最小;100 表示最高质量,压缩程度最低,图片文件最大。可以自己试着去修改其中的值,然后保存图片,直观感受一下,不同的压缩程度对图片质量的影响其实是挺大的。

下图是 cv2.IMWRITE_JPEG_QUALITY 参数为 0 的效果:

检测图片大小

前面说过,写这篇文章的初衷是想把图片放到网页上,那就要求图片文件的大小要小于一定的值,我自己感觉在 200k 以内的图片的加载速度是很快的(并不权威)。那么显然,我们的目标不仅仅是“压缩图片”,而是“压缩图片到指定大小以内”。显然,我们并不希望每压缩一次就去看看图片文件的大小,然后再回来改参数,我们的期望是一步到位,全自动!所以需要在代码运行是检测图片文件的大小是否满足条件,不满足条件的话继续压缩,直到图片小于 200k 为止。

这个其实没有什么技巧,就一个函数,python 的 os 库中有一个函数 os.path.getsize,只需要传入文件名或路径,就可以返回文件的大小,单位为字节。

import os
os.path.getsize("new.jpg")

融合!最终代码!

通过之前所述的几个技巧,为了压缩图片文件大小,我们可以降低图片分辨率、降低图片质量,并用 os.path.getsize 函数监控文件大小,把他们组装在一起就能得到最后的代码:

import cv2
import os

img = cv2.imread("img.jpg")
print(img.shape)

rate = 1280 / img.shape[1]
if rate > 1:
    rate = 1
new_img = cv2.resize(img, (0, 0), fx = rate, fy = rate)
print(new_img.shape)

for i in range(100, 0, -1):
    cv2.imwrite("new.jpg", new_img, [cv2.IMWRITE_JPEG_QUALITY, i])
    print(os.path.getsize("new.jpg"))
    if os.path.getsize("new.jpg") < 200 * 1024:
        print(i)
        break 

上面的代码依然是 3 部分:读取图片→改分辨率→保存图片,我用 rate 表示分辨率缩放的比例,rate 用 1280 除以原图的 列数得到,这样新图的分辨率的行数就是 1280 左右,并且保持了横纵比,不会变形,而如果原图的列数就小于 1280,那就没必要将其放大了,所以加了个 if ,这样的代码显然有一个缺点,那就是没有考虑原图的行数,不过这很容易调整,就无所谓了。在 imwrite 保存新图时,我们可以递减地选择 jpg 图片的质量,直到图片文件大小低于 200kb 为止,上面代码中的 200 * 1024 指的就是 200kb,这当然也可以轻松调整。

生成ico图标

ico 是图标,其实跟本篇关系不大,但是这么小部分的内容又不想开多一篇文章,于是就放在这里吧~

from PIL import Image

default_size_list = [
        (256, 256),
        (128, 128),
        (64, 64),
        (48, 48),
        (32, 32),
        (24, 24),
        (16, 16)
    ]
image = Image.open("img.jpg")
image.save("new.ico", sizes = default_size_list)

就是简单的调用 PIL 库的 Image 模块,生成图标而已,因为一个 ico 文件包含了多个尺寸的图标,所以可以用一个列表来传入我们需要的所有尺寸。

结语

虽然压缩图片是很简单的事情,本篇所述的代码也非常简单。但是自己可以用代码去实现,而别人则去下载软件,这其实是一件非常有成就感的事情。希望你喜欢这篇博客!