本文一步步为你介绍,如何用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)。
如果你对数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。