如何用Python智能批量压缩图片?

in python •  7 years ago 

本文一步步为你介绍,如何用Python自动判断多张图片中哪些超出阈值需要压缩,且保持宽高比。如果你想了解Python图像处理的基础知识,欢迎动手来尝试。

痛点

我喜欢用Markdown写文稿,然后发布到不同写作平台。我的好友数字游民Jarod称其为“矩阵式发布”。能这样做的前提,是Markdown为我们带来了极低的边际发布成本。试想如果每个写作平台,都需要我手动插入20-30张图片,想想都眼晕,我估计立刻会打消发布念头。

我使用七牛作为图床。图片链接成功转换后,选择一款渲染工具,预览文稿格式,看图片、表格、标题等特殊样式是否显示正确。

我曾经用过多种渲染工具。最近我一直在用Md2All

这款工具最大的特点,是能保证粘贴到各个写作平台时,代码不会乱掉。

点击右上方的“复制”按钮,你就可以在任何一个写作平台上,开启富文本编辑器,然后粘贴进去。

工作进行到这一步,已近大功告成。这时,如果你遇到“图片上传失败”的报错,想必会很影响心情。

图片上传失败,原因可能有很多。

许多情况下,只是单纯因为网络拥塞。只要你本着愚公移山的精神,往复重新粘贴,总会好的。

但是微信公众平台是个例外。

你时常会遇到这种情况——就是那两张图片,死活也无法正常传上去。

踩坑多次,不得不手动上传图片后。我终于发现了问题所在——微信公众平台对图片大小有限制。

一旦你要上传的图片超过2M,就无法正常粘贴上传了。

莫非我写作文章时,还要一一检验每张插图的大小?超过阈值的图片压缩,然后再上传?

对我这种插图爱好者来说,这个工作太过琐碎和枯燥了。

你可能会问,不是有许多工具可以批量修改图片大小吗?例如JPEGmini和TinyPNG之类的?

确实有,但是它们不完全符合我的需求。

首先,我并不需要压缩全部图像。压缩后的图片,确实在手机上看起来跟原图毫无区别。但我用的图片,很多是教程里的示例。学生可能需要放大到一定程度,甚至要在大屏幕上打开,来查看代码或者运行结果的细节。只要原图没超过2M,还是保持原貌比较稳妥。

其次,我懒。每次写完文章,还得手动运行一个应用,找出这篇文章对应的图片,拖动进去……不好意思,这活儿我懒得干。

幸好,凡是简单重复的枯燥活儿,都是电脑的拿手好戏。否则我们学编程干什么?

我用Python做个程序,替我找出全部大于2M的图片,进行压缩。压缩的时候,须要保持图片的宽高比例。

如果你对Python图像预处理功能比较感兴趣,不妨跟着我的介绍,一起试试看。

数据

我已经为你准备好了样例图片和执行代码,并且存储在了一个Github项目中。请访问这个链接,下载压缩包后,解压查看。

可以看到,在image目录下,有2个png格式的图像文件。

我们打开来看看,一张cat.png是可爱的猫咪。

另一张,是小松鼠。

猜猜哪张图片更大?

小松鼠这张图片,尺寸低于2M。猫咪那张,却有2.9M,不符合微信公众平台的要求。

我们下面要用Python自行判断这些图片中,哪些超过了2M,需要进行压缩。

然后,对超过2M的图片,按照原先的宽高比压缩后,存储到一个指定的文件夹里面去。

环境

我们使用Python集成运行环境Anaconda。

请到这个网址 下载最新版的Anaconda。下拉页面,找到下载位置。根据你目前使用的系统,网站会自动推荐给你适合的版本下载。我使用的是macOS,下载文件格式为pkg。

下载页面区左侧是Python 3.6版,右侧是2.7版。请选择2.7版本。

双击下载后的pkg文件,根据中文提示一步步安装即可。

安装好Anaconda后,我们还需要确保安装几个必要的软件包。

请到你的“终端”(Linux, macOS)或者“命令提示符”(Windows)下面,进入咱们刚刚下载解压后的样例目录。

执行以下命令:

pip install -U PIL
pip install -U glob

安装完毕后,执行:

jupyter notebook

这样就进入到了Jupyter笔记本环境。我们新建一个Python 2笔记本。

这样就出现了一个空白笔记本。

点击左上角笔记本名称,修改为有意义的笔记本名“demo-python-resize-image”。

准备工作完毕,下面我们就可以用Python读入并处理图像文件了。

代码

我们首先读入几个后面将用到的软件包。

from glob import glob
from PIL import Image
import os

然后,我们指定图片来源目录。因为图片存储在了样例目录的子目录image下面,所以只需要指定为"image"就好了。

source_dir = 'image'

下面我们设置压缩后图片的输出目录。这里为了对比清晰,我们将其设定为output,也是样例目录的子目录。注意此时这个目录还不存在。我们后面会做处理。

target_dir = 'output'

下面,是关键环节之一。我们须要遍历image目录,找出全部的图片名称。

这里我们用到的,是glob软件包。其中的glob函数可以在我们指定的目录里,寻找所有符合要求的文件。

filenames = glob('{}/*'.format(source_dir))

我们使用了星号(*)作为通配符,意味着我们要查找image目录下所有文件的名称。

输出filenames试试看。

print(filenames)
['image/squirrel.png', 'image/cat.png']

可见filenames是个列表,里面包含了咱们需要处理的全部图片文件。

下面,我们就来尝试检测每张图片的大小。

for filename in filenames:
    with Image.open(filename) as im:
        width, height = im.size
        print(filename, width, height, os.path.getsize(filename))

我们遍历filenames中的所有图片路径,用PIL对象的size属性获得图片的宽度(width)和高度(height)数值。用os.path.getsize()函数来获取文件大小。

然后,我们把这些内容按文件分别打印出来。

('image/squirrel.png', 1024, 768, 1466487)
('image/cat.png', 2067, 1163, 2851538)

因为我们需要判断某张图片的大小是否超出微信公众平台设置的2M阈值,因此我们需要计算一下,2M阈值换算成比特,到底是个多大的的数字,以便后面的比对。

2*1024*1024

计算结果如下:

2097152

显然,刚才的打印结果里面,cat.png图像超出了这个阈值。

我们心里有数了。

下面就把阈值(threshold)设置为这个数值。

threshold = 2*1024*1024

我们来看看自己的直觉和程序判断的实际情况是否一致:

for filename in filenames:
    filesize = os.path.getsize(filename)
    if filesize >= threshold:
        print(filename)

此处我们要求Python打印全部超出阈值的文件路径。结果如下:

image/cat.png

测试结果正确。程序只需要调整猫咪照片的尺寸。

正式进行压缩和输出之前,我们需要建立输出目录。虽然前面我们设定了,这个子目录叫做output,但是实际的演示目录里,它还尚未创建。

我们先用os.path.exists()函数判定这个目录是否存在。当判定为不存在时,我们采用os.makedirs()函数来创建它。

if not os.path.exists(target_dir):
    os.makedirs(target_dir)

下面我们计算一下,对需要压缩的图片,新的宽度和高度应该是多少。

for filename in filenames:
    filesize = os.path.getsize(filename)
    if filesize >= threshold:
        print(filename)
        with Image.open(filename) as im:
            width, height = im.size
            new_width = 1024
            new_height = int(new_width * height * 1.0 / width)
            print('adjusted size:', new_width, new_height)

我们把新的宽度设置为了1024,然后按照同等宽高比例算出新的高度取值。

注意这里宽度和高度必须设置为整数类型,否则会报错。

输出结果如下:

image/cat.png
('adjusted size:', 1024, 576)

为了把猫咪照片压缩为宽度1024的图片,我们需要设定高度为576,以保证压缩后的图片与原始图片的宽高比一致。

下面我们续写函数,正式调用PIL的resize函数将新的图片设定为新的宽度和高度数值。然后,我们使用PIL的save函数,把生成的图片存储到指定的路径。

for filename in filenames:
    filesize = os.path.getsize(filename)
    if filesize >= threshold:
        print(filename)
        with Image.open(filename) as im:
            width, height = im.size
            new_width = 1024
            new_height = int(new_width * height * 1.0 / width)
            resized_im = im.resize((new_width, new_height))
            output_filename = filename.replace(source_dir, target_dir)
            resized_im.save(output_filename)

输出结果还是需要压缩的图片路径。

image/cat.png

压缩成功了吗?

我们打开样例目录看看。

可以看到,output子目录已经自动生成。里面有一张图片。名称依然是cat.png。它的大小已经变成了836KB。我们打开它,看看显示是否正确。

依然是这张可爱的猫咪。看不出与原图有什么显著的区别,而且宽高比也正常。测试成功。

整合

但是这里,我们还需要完成一个重要步骤——把之前的代码进行整合。

许多初学者写代码,总会忽略这一步。

虽然你的代码已经成功完成了预期的任务,但如不及时进行整理,过一段时间再来看,你会抓不住头绪。

想想看,等你回来的时候,你的Jupyter Notebook是这个样子的:

你不仅会忘了不同函数之间的调用关系,而且对于哪些参数需要设定,都一头雾水。

没错,这就是人脑的工作特点——我们会遗忘。

所以,趁热打铁,把你做过的功能进行模块化整合很有必要。

整合后,你实现的功能就成了一个有机的整体,只通过参数和外部交互。你只需要用注释告诉自己参数设置的含义。后面再需要调用相关功能的时候,就可以直接通过参数变化,拿来就用了。

趁着记忆犹新,咱们把刚刚全部的功能整合到一个函数里面。

def resize_images(source_dir, target_dir, threshold):
    filenames = glob('{}/*'.format(source_dir))
    if not os.path.exists(target_dir):
        os.makedirs(target_dir)
    for filename in filenames:
        filesize = os.path.getsize(filename)
        if filesize >= threshold:
            print(filename)
            with Image.open(filename) as im:
                width, height = im.size
                new_width = 1024
                new_height = int(new_width * height * 1.0 / width)
                resized_im = im.resize((new_width, new_height))
                output_filename = filename.replace(source_dir, target_dir)
                resized_im.save(output_filename)

这个函数暴露给外部的接口,是3个参数:

  • source_dir:图片源目录
  • target_dir:压缩图片输出目录
  • threshold:阈值

检查一下,我们会发现不对劲的地方——虽然阈值是我们将来可以调整的选项,但是压缩的时候,图片的宽度却是手动设定的数值(1024)。这样将来面对一个阈值高出3倍的写作平台,我们依然把图片压缩到这么小,似乎有些矫枉过正。

另外,如果这张图片是那种极为长的图,那即便宽度不是很长,也可能会因为高度超出阈值。单纯调整宽度到1024,也许会失效。

解决办法也很简单,我们设置高度,然后对应调整宽度。

你可以看到,因为我们把代码集成整理在一处,许多原先我们可能考虑不周的问题,此时就纷纷显现了出来。

了解了问题所在,我们来调整一下代码。

我们因为要通过阈值计算宽度或者高度,所以需要引入数学计算模块。

import math

调整后的函数如下:

def resize_images(source_dir, target_dir, threshold):
    filenames = glob('{}/*'.format(source_dir))
    if not os.path.exists(target_dir):
        os.makedirs(target_dir)
    for filename in filenames:
        filesize = os.path.getsize(filename)
        if filesize >= threshold:
            print(filename)
            with Image.open(filename) as im:
                width, height = im.size
                if width >= height:
                    new_width = int(math.sqrt(threshold/2))
                    new_height = int(new_width * height * 1.0 / width)
                else:
                    new_height = int(math.sqrt(threshold/2))
                    new_width = int(new_height * width * 1.0 / height)
                resized_im = im.resize((new_width, new_height))
                output_filename = filename.replace(source_dir, target_dir)
                resized_im.save(output_filename)

这样,将来无论你的图片目录在哪里,你要满足哪个写作平台的图片大小要求,都可以通过简单设置这样几个数值,调用函数来完成新需求。

我们尝试用原先的参数取值,执行一次。

执行之前,我们删除掉output目录,以测试功能。

然后执行模块化之后的函数。

resize_images(source_dir, target_dir, threshold)

执行时,依然只是输出需要压缩的文件路径。

image/cat.png

检查刚刚又重新生成的output目录,猫咪照片呢?

没问题。不仅显示正常,而且大小也已经正常压缩。

小结

总结一下,通过本文我们接触到了以下知识点:

  • 如何利用glob软件包遍历指定目录,获得符合条件的全部文件路径列表;
  • 如何用PIL图像处理工具读取图像文件,检查宽度、高度,重新设定图像大小,并且存储新生成的图像;
  • 如何用os函数库检查文件或目录是否存在,创建目录,以及获取文件尺寸。

更重要的,是我们尝试了如何用Python这一脚本语言,帮我们智能化做出判断,并且在后台完成琐碎的重复操作。

另外,你应该已经了解了,完成功能并不意味着完事大吉。为了让自己的代码可以充分重用、易于共享并提高效能,你需要梳理与整合代码,将其充分模块化,只曝露输入输出接口给用户(包括将来的自己),避免固定取值设置。

讨论

你之前遇到过需要智能批量调整图片大小的问题吗?你是如何解决的?用过哪些工具?它们能自动帮你判断图片是否需要压缩吗?欢迎留言,把你的经验和思考分享给大家,我们一起交流讨论。

如果你对我的文章感兴趣,欢迎点赞,并且微信关注和置顶我的公众号“玉树芝兰”(nkwangshuyi)

如果本文可能对你身边的亲友有帮助,也欢迎你把本文通过微博或朋友圈分享给他们。让他们一起参与到我们的讨论中来。

延伸阅读

如何高效入门数据科学?


创作不易,感谢打赏!

如果喜欢我的文章,请微信扫描下方二维码,关注并置顶我的公众号“玉树芝兰”。

欢迎微信扫码加入我的“知识星球”第二季。第一时间分享我的发现和思考,优先解答你的疑问。

或者,你也可以使用下面的小程序向我提问。

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you very much. Your post gives me more valuable information about writing.