為提高用戶體驗,Yelp 是如何無損壓縮圖片的
(點擊
上方藍字
,快速關注我們)
編譯:伯樂在線 - iScream
如有好文章投稿,請點擊 → 這裡了解詳情
Yelp 擁有超過 1 億張由用戶生成的照片,這些照片從晚餐、理髮,到我們最新的功能之一:yelfies。這些圖像佔據了用戶 APP 和網站的大部分帶寬,這意味著存儲和傳輸的巨大成本。為了向用戶提供最好的體驗,我們努力優化這些圖片並將其平均大小縮小了 30%。這樣節省了用戶的時間和帶寬,並降低了為這些圖像提供服務的成本。哦,我們這樣做都沒有降低圖像的質量呢!
背景
Yelp 已經幫用戶存儲上傳的照片超過 12 年了。我們那些將無損格式的圖片(PNG,GIF)保存成 PNG 格式,而所有其他的格式的圖片則保存成 JPEG 格式。我們保存圖片用的是 Python 和 Pillow,並使用如下代碼片段,開啟了我們照片上傳的故事:
# do a typical thumbnail, preserving aspect ratio
new_photo
=
photo
.
copy
()
new_photo
.
thumbnail
(
(
width
,
height
),
resample
=
PIL
.
Image
.
ANTIALIAS
,
)
thumbfile
=
cStringIO
.
StringIO
()
save_args
=
{
"format"
:
format
}
if
format
==
"JPEG"
:
save_args
[
"quality"
]
=
85
new_photo
.
save
(
thumbfile
,
**
save_args
)
以此作為起點,我們開始研究潛在的文件大小優化方法,可以讓我們在不損失質量的前提下使用。
優化
首先,我們不得不決定是自己來處理這個問題,還是讓 CDN 提供商用他們的魔力來神奇地改變我們的照片。在優先保證高質量的內容前提下,去評估眾多選項並使得潛在的尺寸與質量相互抵消是有意義的。我們先行調查了當前照片文件大小縮小的現狀 – 可以做出什麼改變,以及與每個改變相關聯的大小/質量下降。隨著這項調查的完成,我們決定從三大類方法來著手。這篇文章剩下的部分解釋了我們做了什麼,並在每項優化中獲得了多少收益。
1.Pillow中的改變
優化標誌
漸進的JPEG
2.應用照片邏輯的更改
大型 PNG 格式圖的檢測
動態 JPEG 質量
3.JPEG編碼器更改
Mozjpeg(網格量化,自定義量化矩陣)
Pillow 中的改變
標誌位優化
這是我們最簡單的更改之一:啟用 Pillow 中的設置,以 CPU 耗時為代價(optimize = True)來節約更多的文件大小。 由於該權衡的本質,這樣做並不會影響圖像質量。
對於 JPEG 格式的圖片,該標誌位會指示編碼器,通過掃描每張圖片做一次附加的遍歷,來找到最佳霍夫曼編碼。對每張圖片的第一遍掃描,我們不是寫入文件,而是計算每個值的出現統計,這是計算理想編碼所需要信息。PNG 內部使用 zlib,因此在這種情況下,標誌位優化能有效地指示編碼器使用 gzip -9,而不是 gzip -6。
這是一項簡單的改進,但事實證明它並非靈丹妙藥,只減少百分之幾的文件大小。
漸進式 JPEG
將圖像保存為 JPEG 格式時,你可以選擇幾種不同的類型:
從上到下載入的 JPEG 標準圖像。
漸進式 JPEG 圖像,從較模糊載入到較不模糊。Pillow(progressive = True)可以輕鬆地啟用漸進式選項。 因而,人類肉眼可感知到性能有了提升(也就是說,當圖像部分缺失時,更容易注意到它不是完全銳利的)。
此外,漸進式文件打包方式通常會使得文件尺寸減少。如維基百科文章中所詳盡解釋的那樣,JPEG 格式在 8×8 像素塊上使用 Z 字形模式進行熵編碼。當這些像素塊的值被解包並按順序排列時,我們通常首先得到的是非零數字,然後是 0 的序列,用該模式對於圖像中的每個 8×8 塊進行重複和交織。在使用漸進編碼時,解開的像素塊的順序發生了改變。每個塊值較高的數字會首先出現文件中(它給出了漸進式圖像最早掃描到的不同阻塞),並且那些能增加出色細節的小數字(包括更多的 0),它們的跨度將更長,並一直持續到末位。圖像數據的這種重新排序不會改變圖像本身,而是增加了在一行中的可能存在的 0 的數量(這樣可以更容易地壓縮)。
與由用戶貢獻的美味甜甜圈圖像做比較(點擊查看大圖):
(1)標準 JPEG 格式圖渲染的模擬
(2)漸進式 JPEG 格式圖渲染的模擬。
應用照片邏輯的改變
大型 PNG 格式圖片的檢測
Yelp 為用戶生成的內容提供了兩種圖像格式 – JPEG 和 PNG。JPEG 是一種很好的照片格式,但通常會與高對比度設計內容(如 logo)相結合。相比之下,PNG 是完全無損的,非常適合圖形,但對於那些小失真不可見的照片來說佔用的空間太大了。在用戶上傳實際照片是 PNG 格式的情況下,我們識別這些文件並將其另存為 JPEG 格式,可節省下大量的存儲空間。Yelp 上一些常見的 PNG 照片來源於移動設備由和應用程序拍攝的截圖,這些照片是通過添加特效或邊框修飾了的。
(左)一個典型的具有 logo 和邊框的複合 PNG 格式的上傳圖。 (右)一個典型的來自於屏幕截圖的 PNG 格式的上傳圖。
我們想減少這些不必要的 PNG 格式的圖片數量,但重要的是要避免矯枉過正或降低了 logo 和圖形等的質量。我們應該如何區分出一張圖片中屬於照片的部分呢?從像素層面可以做到嗎?
使用了 2500 張圖片作為實驗樣本後,我們發現文件大小和獨特像素的結合可以很好地區分照片。我們用我們最大的解析度生成候選縮略圖,看看輸出PNG文件是否大於 300 KiB。如果是,我們還將檢查圖像內容,查看是否有超過 2^16 種獨特顏色(Yelp 將 RGBA 格式的上傳圖片轉成 RGB 格式,但如果沒這樣做的話,我們也會檢查)。
在實驗數據集中,那些手動調整閾值來定義成「 bigness 」佔了有可能縮小的文件大小的 88%(即如果我們要轉換所有圖像,我們預期的文件大小的縮小),這是在不會導致轉換後圖形的任何假陽性的前提下進行的。
動態的 JPEG 質量
縮小 JPEG 文件大小的第一個也是最為人所知的一種方式是稱為質量的設置。許多能保存 JPEG 格式圖片的應用程序會將質量指定為一個數字。
質量這個詞有點抽象。事實上,一幅 JPEG 格式的圖像的每個顏色通道都有單獨的質量。質量等級從 0 到 100 映射到顏色通道的不同量化表,取決於丟失了多少數據(通常是高頻成分)。信號域中的量化是 JPEG 編碼過程中丟失信息的步驟之一。
減小文件大小的最簡單的方法是降低圖像的質量,這會引入更多的雜訊。不是每張圖片在一個給定的相同質量水平上都會丟失相同的信息量。
我們可以為每張圖片動態地選擇一種質量優化設置,找到質量和存儲大小之間的理想平衡。有兩種方法可以做到這一點:
自下而上:這些是通過處理 8 x 8 像素塊級別的圖像生成調諧量化表的演算法。它們計算出丟失了多少理論上的質量,以及丟失的數據如何放大或消除,對人眼可見失真的多或少。
自上而下的:這些演算法是將整張圖片與其原始無失真版本進行比較,檢測丟失了多少信息。通過迭代生成具有不同質量設置的候選圖像,我們可以選擇其中一張圖片,作為滿足某種評估演算法的最低評估水平的圖像。
我們評估了一種自下而上的演算法,在實驗中,在我們希望使用的質量範圍的上限內沒有產生合適的結果(儘管它似乎仍然具有中檔圖像質量的潛力, 這時編碼器可以開始更冒險地去丟棄它的位元組)。關於這一策略的眾多學術論文在90年代初就有發表,該方法對計算能力要求很高,並且走了選項 B 地址這類的捷徑,例如不評估塊之間的交互。
所以我們採取了第二種方法:使用對分的演算法生成不同質量水平的候選圖像,並通過使用 pyssim 函數來計算其結構相似性度量(SSIM),來評估每個候選圖像的質量下降,直到該值處於可配置但閾值也還處於靜態。這允許我們選擇性地降低文件的平均大小(和平均質量),僅僅針對那些已經開始產生肉眼可識別的質量下降的圖像。
在下圖中,我們繪製了用3種不同質量方法生成的 2500 張圖像的SSIM值的表。
1、用一種質量為 85 的初始方法作出原始圖像,使用藍線來繪製。
2、一種能降低文件大小,質量改為 80 的替代方法,用紅線來繪製。
3、我們在最後選用了一種方法,使用橙色繪製動態質量的 SSIM 為 80 – 85 的圖,即選擇質量為 80 到 85(含)的圖像,這是基於一種滿足或超過的 SSIM 比率:該比率是預先計算的、使轉換髮生在圖像範圍中間的某個靜態值。這樣我們可以降低文件的平均大小,而不會降低那些質量最差的圖片的質量。
3 種不同質量策略下 2500 張圖片的 SSIM 值圖
SSIM?
有不少圖像質量演算法試圖模仿人類視覺系統。我們已經評估過其中的許多演算法,並認為 SSIM 雖然較為古老,但基於以下幾個特徵是最適合於迭代優化的:
1、對 JPEG 量化誤差敏感
2、快速,簡單的演算法
3、可以直接對 PIL 原生圖像對象進行計算,而不是將圖像轉換為 PNG 格式並將其傳遞給 CLI 應用程序(參見#2)
動態質量的代碼示例:
import cStringIO
import
PIL
.
Image
from ssim import compute_ssim
def get_ssim_at_quality
(
photo
,
quality
)
:
"""Return the ssim for this JPEG image saved at the specified quality"""
ssim_photo
=
cStringIO
.
StringIO
()
# optimize is omitted here as it doesn"t affect
# quality but requires additional memory and cpu
photo
.
save
(
ssim_photo
,
format
=
"JPEG"
,
quality
=
quality
,
progressive
=
True
)
ssim_photo
.
seek
(
0
)
ssim_score
=
compute_ssim
(
photo
,
PIL
.
Image
.
open
(
ssim_photo
))
return
ssim_score
def _ssim_iteration_count
(
lo
,
hi
)
:
"""Return the depth of the binary search tree for this range"""
if
lo
>=
hi
:
return
0
else
:
return
int
(
log
(
hi
-
lo
,
2
))
+
1
def jpeg_dynamic_quality
(
original_photo
)
:
"""Return an integer representing the quality that this JPEG image should be
saved at to attain the quality threshold specified for this photo class.
Args:
original_photo - a prepared PIL JPEG image (only JPEG is supported)
"""
ssim_goal
=
0.95
hi
=
85
lo
=
80
# working on a smaller size image doesn"t give worse results but is faster
# changing this value requires updating the calculated thresholds
photo
=
original_photo
.
resize
((
400
,
400
))
if
not
_should_use_dynamic_quality
()
:
default_ssim
=
get_ssim_at_quality
(
photo
,
hi
)
return
hi
,
default_ssim
# 95 is the highest useful value for JPEG. Higher values cause different behavior
# Used to establish the image"s intrinsic ssim without encoder artifacts
normalized_ssim
=
get_ssim_at_quality
(
photo
,
95
)
selected_quality
=
selected_ssim
=
None
# loop bisection. ssim function increases monotonically so this will converge
for
i
in
xrange
(
_ssim_iteration_count
(
lo
,
hi
))
:
curr_quality
=
(
lo
+
hi
)
// 2
curr_ssim
=
get_ssim_at_quality
(
photo
,
curr_quality
)
ssim_ratio
=
curr_ssim
/
normalized_ssim
if
ssim_ratio
>=
ssim_goal
:
# continue to check whether a lower quality level also exceeds the goal
selected_quality
=
curr_quality
selected_ssim
=
curr_ssim
hi
=
curr_quality
else
:
lo
=
curr_quality
if
selected_quality
:
return
selected_quality
,
selected_ssim
else
:
default_ssim
=
get_ssim_at_quality
(
photo
,
hi
)
return
hi
,
default_ssim
還有一些關於這種技術的其他博客文章,這裡是柯爾特·麥卡尼斯(Colt Mcanlis)的一篇博文。當我們發布這篇博客的時候,Etsy 已經發表了一篇了!擊掌吧,更快的網路!
JPEG 編碼器的更改
Mozjpeg 是 libjpeg-turbo 的一個開源分支,它採取以時間換空間的方法,通過更長時間的運算換取更加優化的文件尺寸。這種方法很好地與離線批處理方法結合以重新生成圖像。一些更昂貴的演算法用了比 libjpeg-turbo 多出 3 – 5 倍的時間,使得圖像更小了一些!
mozjpeg 的區別之一是使用一種替代量化表。如上所述,質量是用於每個顏色通道的量化表的抽象。所有符號都指向默認的 JPEG 量化表,因而它很容易被擊敗。用這個詞來說,
JPEG spec:
這些表僅作為示例,並不一定適用於任何特定應用。
那麼自然地,不要驚訝於你知道這些表是大多數編碼器使用的默認值…
Mozjpeg 經歷了為我們的替代表做基準測試的麻煩時期,並在之後成為了它所創建的圖像中,能用到的性能最好的通用替代品。
Mozjpeg + Pillow
大多數的 Linux 發行版都默認安裝了 libjpeg。所以在 Pillow 下使用 mozjpeg 在默認情況下不起作用,但配置起來也不是很困難。當您構建 mozjpeg 時,請使用–with-jpeg8 標誌位,並確保可以通過 Pillow 鏈接到它。如果您使用Docker,您可能會有一個 Dockerfile,如:
FROM
ubuntu
:
xenial
RUN
apt
-
get
update
&&
DEBIAN_FRONTEND
=
noninteractive
apt
-
get
-
y
--
no
-
install
-
recommends
install
# build tools
nasm
build
-
essential
autoconf
automake
libtool
pkg
-
config
# python tools
python
python
-
dev
python
-
pip
python
-
setuptools
# cleanup
&&
apt
-
get
clean
&&
rm
-
rf
/
var
/
lib
/
apt
/
lists
/* /
tmp
/* /
var
/
tmp
/*
# Download and compile mozjpeg
ADD
https
:
//github.com/mozilla/mozjpeg/archive/v3.2-pre.tar.gz /mozjpeg-src/v3.2-pre.tar.gz
RUN
tar
-
xzf
/
mozjpeg
-
src
/
v3
.
2
-
pre
.
tar
.
gz
-
C
/
mozjpeg
-
src
/
WORKDIR
/
mozjpeg
-
src
/
mozjpeg
-
3.2
-
pre
RUN
autoreconf
-
fiv
&&
.
/
configure
--
with
-
jpeg8
&&
make install
prefix
=/
usr
libdir
=/
usr
/
lib64
RUN
echo
"/usr/lib64n"
> /
etc
/
ld
.
so
.
conf
.
d
/
mozjpeg
.
conf
RUN
ldconfig
# Build Pillow
RUN pip install
virtualenv
&&
virtualenv
/
virtualenv_run
&& /
virtualenv_run
/
bin
/
pip
install
--
upgrade
pip
&& /
virtualenv_run
/
bin
/
pip
install
--
no
-
binary
=:
all
:
Pillow
==
4.0.0
以上!使用它,您將可以在正常圖片工作流程中,使用由 mozjpeg 支持的 Pillow。
影響
這些改進中的每一項對我們的提升分別是多少呢?我們隨機抽取 2,500 張 Yelp 上的商業照片開始了此項研究,測試照片大小在經過我們的處理流程後有怎樣的變化。
1、通過改變 Pillow 的設置,可以將圖片大小減小 4.5%
2、大型 PNG 格式圖片檢測可以將圖片大小減小 6.2%
3、動態質量可以將圖片大小減小 4.5%
4、切換到 mozjpeg 編碼器可以將圖片大小減小 13.8%
這些改進使得圖像文件的平均大小減少了約 30%,並被我們應用到我們最大和最常見的圖像解析度上,使網站能為用戶提供更快速的服務,並且每天在數據傳輸上節省了 TB 級別的容量。按照 CDN 來衡量:
從 CDN(結合非圖像靜態內容)測量的平均文件大小。
我們還沒做的
本節旨在向您介紹可能能夠做出的一些常見改進,有的是因為它們和 Yelp 使用的工具並無關係,有些是因為我們權衡後決定不做。
子採樣
子採樣是決定網頁圖像的質量和文件大小的主要因素。對於子抽樣的更詳盡的描述可以在網上找到,但在這個博客文章我們只要說以 4:1:1(這是 Pillow 的默認值,沒有指定任何其他內容)進行子採樣就夠了,所以我們無法了解能進一步縮小多少。
有損的 PNG 編碼
在了解我們對 PNG 格式圖片做了什麼之後,用例如 pngmini 這樣的有損編碼器,選擇將這些圖片中一部分保留為 PNG 格式,可能是有意義的,但是我們選擇將其重新保存成 JPEG 格式。這是一個結果看來合理的替代選項,根據作者的說法,未修改的 PNG 格式圖片的文件大小縮小了 72-85 %。
動態內容類型
為更多的現代化內容類型提供支持,如 WebP 或 JPEG2k 肯定是我們的未來要做的。而一旦這個假設的項目實行了,就會有大量的用戶要求為現有已優化的 JPEG / PNG 圖像做這些,這將繼續使該項工作非常值得做。
SVG
我們在網站上的許多地方使用 SVG,就像我們設計師創建的靜態資產,這已成為我們的指導風格。雖然這種格式和優化工具(如 svgo)有助於減少網站頁面的開銷,但它與我們在這裡所做的工作無關。
供應商的魔術
能提供圖像傳送/調整大小/裁剪/轉碼服務的供應商太多,包括開源的 thumbor。也許這是支持響應式圖像,動態內容類型的最簡單方法,並且在將來仍然能讓我們保持在技術前沿。而現在我們的解決方案仍然是獨立的。
拓展閱讀
這裡列出的兩本書在這個領域歷史上是絕對能站穩腳跟的,強烈建議您進一步閱讀這些書籍。
High Performance Images
Designing for Performance
看完本文有收穫?請轉
發分享給更多人
關注「P
ython開發者」,提升Python技能
※BAT 面試官帶你刷真題、過筆試
※K-means 在 Python 中的實現
※選擇一個 Python Web 框架:Django vs Flask vs Pyramid
TAG:Python開發者 |