本文一步步為你介紹,如何用Python自動(dòng)判斷多張圖片中哪些超出閾值需要壓縮,且保持寬高比。如果你想了解Python圖像處理的基礎(chǔ)知識(shí),歡迎動(dòng)手來(lái)嘗試。

痛點(diǎn)
我喜歡用Markdown寫(xiě)文稿,然后發(fā)布到不同寫(xiě)作平臺(tái)。我的好友數(shù)字游民Jarod稱其為“矩陣式發(fā)布”。能這樣做的前提,是Markdown為我們帶來(lái)了極低的邊際發(fā)布成本。試想如果每個(gè)寫(xiě)作平臺(tái),都需要我手動(dòng)插入20-30張圖片,想想都眼暈,我估計(jì)立刻會(huì)打消發(fā)布念頭。
我使用七牛作為圖床。圖片鏈接成功轉(zhuǎn)換后,選擇一款渲染工具,預(yù)覽文稿格式,看圖片、表格、標(biāo)題等特殊樣式是否顯示正確。
我曾經(jīng)用過(guò)多種渲染工具。最近我一直在用Md2All。
這款工具最大的特點(diǎn),是能保證粘貼到各個(gè)寫(xiě)作平臺(tái)時(shí),代碼不會(huì)亂掉。

點(diǎn)擊右上方的“復(fù)制”按鈕,你就可以在任何一個(gè)寫(xiě)作平臺(tái)上,開(kāi)啟富文本編輯器,然后粘貼進(jìn)去。
工作進(jìn)行到這一步,已近大功告成。這時(shí),如果你遇到“圖片上傳失敗”的報(bào)錯(cuò),想必會(huì)很影響心情。
圖片上傳失敗,原因可能有很多。
許多情況下,只是單純因?yàn)榫W(wǎng)絡(luò)擁塞。只要你本著愚公移山的精神,往復(fù)重新粘貼,總會(huì)好的。
但是微信公眾平臺(tái)是個(gè)例外。
你時(shí)常會(huì)遇到這種情況——就是那兩張圖片,死活也無(wú)法正常傳上去。
踩坑多次,不得不手動(dòng)上傳圖片后。我終于發(fā)現(xiàn)了問(wèn)題所在——微信公眾平臺(tái)對(duì)圖片大小有限制。
一旦你要上傳的圖片超過(guò)2M,就無(wú)法正常粘貼上傳了。
莫非我寫(xiě)作文章時(shí),還要一一檢驗(yàn)每張插圖的大???超過(guò)閾值的圖片壓縮,然后再上傳?
對(duì)我這種插圖愛(ài)好者來(lái)說(shuō),這個(gè)工作太過(guò)瑣碎和枯燥了。
你可能會(huì)問(wèn),不是有許多工具可以批量修改圖片大小嗎?例如JPEGmini和TinyPNG之類(lèi)的?
確實(shí)有,但是它們不完全符合我的需求。
首先,我并不需要壓縮全部圖像。壓縮后的圖片,確實(shí)在手機(jī)上看起來(lái)跟原圖毫無(wú)區(qū)別。但我用的圖片,很多是教程里的示例。學(xué)生可能需要放大到一定程度,甚至要在大屏幕上打開(kāi),來(lái)查看代碼或者運(yùn)行結(jié)果的細(xì)節(jié)。只要原圖沒(méi)超過(guò)2M,還是保持原貌比較穩(wěn)妥。
其次,我懶。每次寫(xiě)完文章,還得手動(dòng)運(yùn)行一個(gè)應(yīng)用,找出這篇文章對(duì)應(yīng)的圖片,拖動(dòng)進(jìn)去……不好意思,這活兒我懶得干。
幸好,凡是簡(jiǎn)單重復(fù)的枯燥活兒,都是電腦的拿手好戲。否則我們學(xué)編程干什么?
我用Python做個(gè)程序,替我找出全部大于2M的圖片,進(jìn)行壓縮。壓縮的時(shí)候,須要保持圖片的寬高比例。
如果你對(duì)Python圖像預(yù)處理功能比較感興趣,不妨跟著我的介紹,一起試試看。
數(shù)據(jù)
我已經(jīng)為你準(zhǔn)備好了樣例圖片和執(zhí)行代碼,并且存儲(chǔ)在了一個(gè)Github項(xiàng)目中。請(qǐng)?jiān)L問(wèn)這個(gè)鏈接,下載壓縮包后,解壓查看。

可以看到,在image目錄下,有2個(gè)png格式的圖像文件。
我們打開(kāi)來(lái)看看,一張cat.png是可愛(ài)的貓咪。

另一張,是小松鼠。

猜猜哪張圖片更大?
小松鼠這張圖片,尺寸低于2M。貓咪那張,卻有2.9M,不符合微信公眾平臺(tái)的要求。
我們下面要用Python自行判斷這些圖片中,哪些超過(guò)了2M,需要進(jìn)行壓縮。
然后,對(duì)超過(guò)2M的圖片,按照原先的寬高比壓縮后,存儲(chǔ)到一個(gè)指定的文件夾里面去。
環(huán)境
我們使用Python集成運(yùn)行環(huán)境Anaconda。
請(qǐng)到這個(gè)網(wǎng)址 下載最新版的Anaconda。下拉頁(yè)面,找到下載位置。根據(jù)你目前使用的系統(tǒng),網(wǎng)站會(huì)自動(dòng)推薦給你適合的版本下載。我使用的是macOS,下載文件格式為pkg。

下載頁(yè)面區(qū)左側(cè)是Python 3.6版,右側(cè)是2.7版。請(qǐng)選擇2.7版本。
雙擊下載后的pkg文件,根據(jù)中文提示一步步安裝即可。

安裝好Anaconda后,我們還需要確保安裝幾個(gè)必要的軟件包。
請(qǐng)到你的“終端”(Linux, macOS)或者“命令提示符”(Windows)下面,進(jìn)入咱們剛剛下載解壓后的樣例目錄。
執(zhí)行以下命令:
pip install -U PIL
pip install -U glob
安裝完畢后,執(zhí)行:
jupyter notebook

這樣就進(jìn)入到了Jupyter筆記本環(huán)境。我們新建一個(gè)Python 2筆記本。

這樣就出現(xiàn)了一個(gè)空白筆記本。

點(diǎn)擊左上角筆記本名稱,修改為有意義的筆記本名“demo-python-resize-image”。

準(zhǔn)備工作完畢,下面我們就可以用Python讀入并處理圖像文件了。
代碼
我們首先讀入幾個(gè)后面將用到的軟件包。
from glob import glob
from PIL import Image
import os
然后,我們指定圖片來(lái)源目錄。因?yàn)閳D片存儲(chǔ)在了樣例目錄的子目錄image下面,所以只需要指定為"image"就好了。
source_dir = 'image'
下面我們?cè)O(shè)置壓縮后圖片的輸出目錄。這里為了對(duì)比清晰,我們將其設(shè)定為output,也是樣例目錄的子目錄。注意此時(shí)這個(gè)目錄還不存在。我們后面會(huì)做處理。
target_dir = 'output'
下面,是關(guān)鍵環(huán)節(jié)之一。我們須要遍歷image目錄,找出全部的圖片名稱。
這里我們用到的,是glob軟件包。其中的glob函數(shù)可以在我們指定的目錄里,尋找所有符合要求的文件。
filenames = glob('{}/*'.format(source_dir))
我們使用了星號(hào)(*)作為通配符,意味著我們要查找image目錄下所有文件的名稱。
輸出filenames試試看。
print(filenames)
['image/squirrel.png', 'image/cat.png']
可見(jiàn)filenames是個(gè)列表,里面包含了咱們需要處理的全部圖片文件。
下面,我們就來(lái)嘗試檢測(cè)每張圖片的大小。
for filename in filenames:
with Image.open(filename) as im:
width, height = im.size
print(filename, width, height, os.path.getsize(filename))
我們遍歷filenames中的所有圖片路徑,用PIL對(duì)象的size屬性獲得圖片的寬度(width)和高度(height)數(shù)值。用os.path.getsize()函數(shù)來(lái)獲取文件大小。
然后,我們把這些內(nèi)容按文件分別打印出來(lái)。
('image/squirrel.png', 1024, 768, 1466487)
('image/cat.png', 2067, 1163, 2851538)
因?yàn)槲覀冃枰袛嗄硰垐D片的大小是否超出微信公眾平臺(tái)設(shè)置的2M閾值,因此我們需要計(jì)算一下,2M閾值換算成比特,到底是個(gè)多大的的數(shù)字,以便后面的比對(duì)。
2*1024*1024
計(jì)算結(jié)果如下:
2097152
顯然,剛才的打印結(jié)果里面,cat.png圖像超出了這個(gè)閾值。
我們心里有數(shù)了。
下面就把閾值(threshold)設(shè)置為這個(gè)數(shù)值。
threshold = 2*1024*1024
我們來(lái)看看自己的直覺(jué)和程序判斷的實(shí)際情況是否一致:
for filename in filenames:
filesize = os.path.getsize(filename)
if filesize >= threshold:
print(filename)
此處我們要求Python打印全部超出閾值的文件路徑。結(jié)果如下:
image/cat.png
測(cè)試結(jié)果正確。程序只需要調(diào)整貓咪照片的尺寸。
正式進(jìn)行壓縮和輸出之前,我們需要建立輸出目錄。雖然前面我們?cè)O(shè)定了,這個(gè)子目錄叫做output,但是實(shí)際的演示目錄里,它還尚未創(chuàng)建。
我們先用os.path.exists()函數(shù)判定這個(gè)目錄是否存在。當(dāng)判定為不存在時(shí),我們采用os.makedirs()函數(shù)來(lái)創(chuàng)建它。
if not os.path.exists(target_dir):
os.makedirs(target_dir)
下面我們計(jì)算一下,對(duì)需要壓縮的圖片,新的寬度和高度應(yīng)該是多少。
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)
我們把新的寬度設(shè)置為了1024,然后按照同等寬高比例算出新的高度取值。
注意這里寬度和高度必須設(shè)置為整數(shù)類(lèi)型,否則會(huì)報(bào)錯(cuò)。
輸出結(jié)果如下:
image/cat.png
('adjusted size:', 1024, 576)
為了把貓咪照片壓縮為寬度1024的圖片,我們需要設(shè)定高度為576,以保證壓縮后的圖片與原始圖片的寬高比一致。
下面我們續(xù)寫(xiě)函數(shù),正式調(diào)用PIL的resize函數(shù)將新的圖片設(shè)定為新的寬度和高度數(shù)值。然后,我們使用PIL的save函數(shù),把生成的圖片存儲(chǔ)到指定的路徑。
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)
輸出結(jié)果還是需要壓縮的圖片路徑。
image/cat.png
壓縮成功了嗎?
我們打開(kāi)樣例目錄看看。

可以看到,output子目錄已經(jīng)自動(dòng)生成。里面有一張圖片。名稱依然是cat.png。它的大小已經(jīng)變成了836KB。我們打開(kāi)它,看看顯示是否正確。

依然是這張可愛(ài)的貓咪。看不出與原圖有什么顯著的區(qū)別,而且寬高比也正常。測(cè)試成功。
整合
但是這里,我們還需要完成一個(gè)重要步驟——把之前的代碼進(jìn)行整合。
許多初學(xué)者寫(xiě)代碼,總會(huì)忽略這一步。
雖然你的代碼已經(jīng)成功完成了預(yù)期的任務(wù),但如不及時(shí)進(jìn)行整理,過(guò)一段時(shí)間再來(lái)看,你會(huì)抓不住頭緒。
想想看,等你回來(lái)的時(shí)候,你的Jupyter Notebook是這個(gè)樣子的:

你不僅會(huì)忘了不同函數(shù)之間的調(diào)用關(guān)系,而且對(duì)于哪些參數(shù)需要設(shè)定,都一頭霧水。
沒(méi)錯(cuò),這就是人腦的工作特點(diǎn)——我們會(huì)遺忘。
所以,趁熱打鐵,把你做過(guò)的功能進(jìn)行模塊化整合很有必要。
整合后,你實(shí)現(xiàn)的功能就成了一個(gè)有機(jī)的整體,只通過(guò)參數(shù)和外部交互。你只需要用注釋告訴自己參數(shù)設(shè)置的含義。后面再需要調(diào)用相關(guān)功能的時(shí)候,就可以直接通過(guò)參數(shù)變化,拿來(lái)就用了。
趁著記憶猶新,咱們把剛剛?cè)康墓δ苷系揭粋€(gè)函數(shù)里面。
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)
這個(gè)函數(shù)暴露給外部的接口,是3個(gè)參數(shù):
-
source_dir:圖片源目錄 -
target_dir:壓縮圖片輸出目錄 -
threshold:閾值
檢查一下,我們會(huì)發(fā)現(xiàn)不對(duì)勁的地方——雖然閾值是我們將來(lái)可以調(diào)整的選項(xiàng),但是壓縮的時(shí)候,圖片的寬度卻是手動(dòng)設(shè)定的數(shù)值(1024)。這樣將來(lái)面對(duì)一個(gè)閾值高出3倍的寫(xiě)作平臺(tái),我們依然把圖片壓縮到這么小,似乎有些矯枉過(guò)正。
另外,如果這張圖片是那種極為長(zhǎng)的圖,那即便寬度不是很長(zhǎng),也可能會(huì)因?yàn)楦叨瘸鲩撝?。單純調(diào)整寬度到1024,也許會(huì)失效。
解決辦法也很簡(jiǎn)單,我們?cè)O(shè)置高度,然后對(duì)應(yīng)調(diào)整寬度。
你可以看到,因?yàn)槲覀儼汛a集成整理在一處,許多原先我們可能考慮不周的問(wèn)題,此時(shí)就紛紛顯現(xiàn)了出來(lái)。
了解了問(wèn)題所在,我們來(lái)調(diào)整一下代碼。
我們因?yàn)橐ㄟ^(guò)閾值計(jì)算寬度或者高度,所以需要引入數(shù)學(xué)計(jì)算模塊。
import math
調(diào)整后的函數(shù)如下:
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)
這樣,將來(lái)無(wú)論你的圖片目錄在哪里,你要滿足哪個(gè)寫(xiě)作平臺(tái)的圖片大小要求,都可以通過(guò)簡(jiǎn)單設(shè)置這樣幾個(gè)數(shù)值,調(diào)用函數(shù)來(lái)完成新需求。
我們嘗試用原先的參數(shù)取值,執(zhí)行一次。
執(zhí)行之前,我們刪除掉output目錄,以測(cè)試功能。
然后執(zhí)行模塊化之后的函數(shù)。
resize_images(source_dir, target_dir, threshold)
執(zhí)行時(shí),依然只是輸出需要壓縮的文件路徑。
image/cat.png
檢查剛剛又重新生成的output目錄,貓咪照片呢?

沒(méi)問(wèn)題。不僅顯示正常,而且大小也已經(jīng)正常壓縮。
小結(jié)
總結(jié)一下,通過(guò)本文我們接觸到了以下知識(shí)點(diǎn):
- 如何利用glob軟件包遍歷指定目錄,獲得符合條件的全部文件路徑列表;
- 如何用PIL圖像處理工具讀取圖像文件,檢查寬度、高度,重新設(shè)定圖像大小,并且存儲(chǔ)新生成的圖像;
- 如何用os函數(shù)庫(kù)檢查文件或目錄是否存在,創(chuàng)建目錄,以及獲取文件尺寸。
更重要的,是我們嘗試了如何用Python這一腳本語(yǔ)言,幫我們智能化做出判斷,并且在后臺(tái)完成瑣碎的重復(fù)操作。
另外,你應(yīng)該已經(jīng)了解了,完成功能并不意味著完事大吉。為了讓自己的代碼可以充分重用、易于共享并提高效能,你需要梳理與整合代碼,將其充分模塊化,只曝露輸入輸出接口給用戶(包括將來(lái)的自己),避免固定取值設(shè)置。
討論
你之前遇到過(guò)需要智能批量調(diào)整圖片大小的問(wèn)題嗎?你是如何解決的?用過(guò)哪些工具?它們能自動(dòng)幫你判斷圖片是否需要壓縮嗎?歡迎留言,把你的經(jīng)驗(yàn)和思考分享給大家,我們一起交流討論。
喜歡請(qǐng)點(diǎn)贊。還可以微信關(guān)注和置頂我的公眾號(hào)“玉樹(shù)芝蘭”(nkwangshuyi)。
如果你對(duì)數(shù)據(jù)科學(xué)感興趣,不妨閱讀我的系列教程索引貼《如何高效入門(mén)數(shù)據(jù)科學(xué)?》,里面還有更多的有趣問(wèn)題及解法。