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

本文一步步为你介绍,如何用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)

如果你对数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容