[譯] 如何在無損的狀況下讓圖片變的更小

如何在無損的狀況下讓圖片變的更小

Yelp(美國最大點評網站)已經有超過 1 億張用戶上傳的照片了,其中不但有晚餐、理髮等活動的照片還有咱們的新特性照片 -- #yelfies(一種在拍攝時,加上自拍頭像的一種新的拍照方式)。這些圖片佔用了用戶 app 和網站的大多數帶寬,同時也表明着儲存和傳輸的巨大成本。爲了給咱們的用戶最好的用戶體驗,咱們竭盡所能的優化咱們的圖片,最終達到圖片大小平均減小 30%。這不只節省了咱們用戶的時間和帶寬,還減小了咱們的服務器成本。對了,關鍵的是咱們的這個過程是徹底無損的!html

背景

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)複製代碼

下面讓咱們來尋找一些能夠在無損條件下優化文件大小的方法。python

優化

首先,咱們要決定是選擇咱們本身,仍是一個 CDN 提供商 magically change 來處理咱們的圖片。隨着咱們對高質量內容的重視,評估各類方案並在圖片大小和質量之間作出取捨就顯得很是重要了。讓咱們來研究一下當前圖片文件減少的一些方法,咱們能夠作哪些改變以及每種方法咱們能夠減小多少大小和質量。完成這項研究以後,咱們決定了三個主要策略。本文剩下的部分解釋了咱們所作的工做,以及從每次優化中得到的好處。react

  1. Pillow 中的改變
  • 優化 flag
  • 漸進式 JPEG
  1. 更改應用的照片邏輯
  • 大 PNG 檢測
  • JPEG 動態質量
  1. 更換 JPEG 編碼器
  • Mozjpeg (柵格量化,自定義量化矩陣)

Pillow 中的改變

優化 Flag

這是咱們作出的最簡單的改變之一:開啓 Pillow 中負責以 CPU 耗時爲代價節省額外的文件大小的設置 (optimize=True)。因爲本質沒變,全部這對於圖片質量絲毫沒有影響。android

對於 JPEG 來講,對個選項告訴編碼器經過對每一個圖片進行一次額外的掃描以找到最佳的 霍夫曼編碼。第一次,不寫入文件,而是計算每一個值出現的次數,以及能夠計算出理想編碼的必要信息。PNG 內部使用 zlib,因此在這種狀況下優化選項告訴編碼器使用 gzip -9 而不是 gzip -6ios

這是一個很簡單的改變,可是事實證實它也不是銀彈,由於文件大小隻減小了百分之幾。git

漸進式 JPEG

當咱們將一張圖片保存爲 JPEG 時,你能夠從下面的選項中選擇不一樣的類型:github

  • 標準型: JPEG 圖片自上而下載入。
  • 漸進式: JPEG 圖片從模糊到清晰載入。漸進式的選項能夠在 Pillow 中輕鬆的啓用 (progressive=True)。這是一個能明顯感受到的性能提高(就是比起不是清晰的圖片,只加載一半的圖片更容易注意到。)

還有就是漸進式文件的被打包時會有一個小幅的壓縮。更詳細的解釋請看 Wikipedia article,JPEG 格式在 8x8 像素塊上使用鋸齒模式進行熵編碼。當這些像素塊的值被解壓並按順序展開時,你會發現一般狀況下非零的數字會優先出現,而後是零的序列,那個模式會對圖片的每個 8x8 的像素塊進行隔行掃描。使用漸進編碼時,被解壓開的像素塊的順序會逐漸改變。每一個塊中較大的值將會在文件中首先出現,(漸進模式加載的圖片中區分度最高的區域將最先被掃描),而一段較長的小數字,包括許多數字零,將會在最末加載,用於填充細節。這種圖片數據的從新排列不會改變圖片自己,可是確實可能在某一行(這一行能夠被更容易的壓縮)中增長了 0 的數量。算法

一個美味的甜甜圈的圖片的對比(點擊放大):ubuntu

A mock of how a baseline JPEG renders.
A mock of how a baseline JPEG renders.

模擬標準 JPEG 圖片的渲染效果。

A mock of how a progressive JPEG renders.
A mock of how a progressive JPEG renders.

模擬漸進式 JPEG 圖片的渲染效果。

更改應用的照片邏輯

大 PNG 檢測

Yelp 爲用戶上傳的圖片主要提供兩種格式 - JPEG 和 PNG。JPEG 對於照片來講是一個很棒的格式,可是對於高對比度的設計內容,相似 logo,就不那麼優秀了。而 PNG 則是徹底無損的,因此很是適用於圖形類型的圖片,可是對於差別不明顯的圖片又顯得太大了。若是用戶上傳的 PNG 圖片是照片的話(經過咱們的識別),使用 JPEG 格式來存儲就會節省很大的空間。一般狀況下,Yelp 上的 PNG 圖片都是移動設備和 "美圖類" app 的截圖。

(left) A typical composited PNG upload with logo and border. (right) A typical PNG upload from a screenshot.
(left) A typical composited PNG upload with logo and border. (right) A typical PNG upload from a screenshot.

(左邊) 一張明顯的 PNG 合成圖。(右邊) 一張明顯的 PNG 的截圖。

咱們想減小這些沒必要要的 PNG 圖片的數量,但重要的是要避免過分干預,改變格式或者下降圖片質量。那麼,咱們如何來識別一張圖片呢?經過像素嗎?

經過一組 2500 張圖片的實驗樣本,咱們發現文件大小和獨立像素結合起來能夠很好地幫助咱們判斷。咱們在最大分辨率下生成咱們的候選縮略圖,而後看看輸出的 PNG 文件是否大於 300KB。若是是,咱們就檢測圖片內容是否有超過 2^16 個獨立像素(Yelp 會將 RGBA 圖片轉化爲 RGB,即便不轉,咱們也會作這個檢測)。

在實驗數據集中,手動調整定義大圖片的數值能夠減小 88% 的文件大小(也就是說,若是咱們將全部的圖片都轉換的話,咱們預期能夠節約的存儲空間),而且這些調整對圖片是無損的。

JPEG 動態質量

第一個也是最廣爲人知的減少 JPEG 文件大小的方法就是設置 quality。不少應用保存 JPEG 時都會設置一個特定的質量數值。

質量實際上是個很抽象的概念。實際上,一張 JPEG 圖片的每一個顏色通道都有不一樣的質量。質量等級從 0 到 100 在不一樣的顏色通道上都對應不一樣的量化表,同時也決定了有多少信息會丟失。
在信號域量化是 JPEG 編碼中失去信息的第一個步驟。

減小文件大小最簡單的方法其實就是下降圖片的質量,引入更多的噪點。可是在給定的質量等級下,不是每張圖片都會丟失一樣多的信息。

咱們能夠動態地爲每一張圖片設置最優的質量等級,在質量和文件大小之間找到一個平衡點。咱們有如下兩種方法能夠作到這點:

  • Bottom-up: 這些算法是在 8x8 像素塊級別上處理圖片來生成調優量化表的。它們會同時計算理論質量丟失量和和人眼視覺信息丟失量。
  • Top-down: 這些算法是將一整張圖片和它原版進行對比,而後檢測出丟失了多少信息。經過不斷地用不一樣的質量參數生成候選圖片,而後選擇丟失量最小的那一張。

咱們評估了一個 bottom-up 算法,可是到目前爲止,這個算法尚未在咱們的實驗環境下獲得一個滿意的結果(雖然這個算法看上去在中等質量圖片地處理上還有很多發展潛力,由於處理中等質量圖片能夠丟棄更多的信息)。不少關於這個算法的 學術論文 在 90 年代早期發表,可是在這個算力昂貴的時代,bottom-up 算法的實現走了捷徑,好比沒有評估像素塊之間的相互影響。

因此咱們選擇第二種方法:使用二分法在不一樣的質量等級下生成候選圖片,而後使用 pyssim 計算它的結構類似矩陣 (SSIM) 來評估每張候選圖片損失的質量,直到這個值達到非靜態可配置的閾值爲止。這個方法讓咱們能夠有選擇地下降文件大小(和文件質量),可是隻適用於那些即便下降質量用戶也察覺不到的圖片。

在下面的圖表中,咱們畫出了經過 3 個不一樣的質量等級生成的 2500 張圖片的 SSIM 值的圖像。

  1. 藍色的線爲 quality = 85 生成的原始圖。
  2. 紅色的線爲quality = 80 生成的圖。
  3. 最後,橘色的圖是咱們最後使用的動態質量,參數爲 SSIM 80-85。爲一張圖片基於匯合點或者超過 SSIM 比率(一個提早計算好的靜態值,使得轉換髮生在圖像範圍中間的某處)的地方在 80 到 85 (包括 85) 之間選擇一個質量值。這種方法能夠有效地減少圖片大小,可是又不會突破咱們圖片質量要求的底線。

SSIMs of 2500 images with 3 different quality strategies.
SSIMs of 2500 images with 3 different quality strategies.

2500 張 3 種不一樣的質量策略的 SSIM 值。

SSIM

這裏有很多能夠模擬人類視覺系統的圖片質量算法。在評估了不少方法以後,咱們認爲 SSIM 這個方法雖然比較古老,但倒是最適合對這幾個特徵作迭代優化的:

  1. JPEG 量化偏差敏感。
  2. 快速,簡單的算法。
  3. 能夠在 PIL 本地圖片對象上計算,而不須要將圖片轉換成 PNG 格式,並且還能夠經過命令行運行(查看 #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

Mozjpeglibjpeg-turbo 的一個開源分支,是經過執行時間來置換文件的大小的編碼器。這種方法完美的契合離線批處理再生成圖片。在比 libjpeg-turbo 多投入 3 到 5 倍的時間,和一點複雜的算法就可使圖片變的更小了!

mozjpeg 這個編碼器最大的不一樣點就是使用了一張額外的量化表。就像上面提到的,質量是每個顏色通道量化表的一個抽象的概念。默認 JPEG 量化表的全部信號點都十分容易被命中。用 JPEG 指導 中的話說就是:

這些表僅供參考,不能保證在任何應用中都是適用的。

因此說,大部分編碼器的實現默認狀況下使用這些表就不足爲奇了。

Mozipeg已經替咱們掃平了使用基準測試選擇表的麻煩,並使用性能最好的通用替代方案建立圖片。

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/lib64\n" > /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複製代碼

就是這樣!構建完成,你就能夠在圖片處理工做流中使用帶有 mozipeg 的 Pillow 庫了。

影響

那麼這些方法到底帶來了多少提高呢?讓咱們來研究研究,在 Yelp 的圖片庫中隨機抽取 2500 張圖片並使用咱們的工做流來處理,看看文件大小都有什麼變化:

  1. 更改 Pillow 的設置能夠減少 4.5%
  2. 大 PNG 檢測能夠減少 6.2%
  3. 動態質量能夠減少 4.5%
  4. 更換爲 mozjpeg 編碼器能夠減少 13.8%

這些所有加起來可讓圖片大小平均減少大概 30%,而且咱們應用在最大最多見分辨率的圖片上,對於用戶來講,不只咱們的網頁變的更快,同時平均天天還能夠節省兆兆字節的數據傳輸量。從 CDN 上就可見一斑:

Average filesize over time, as measured from the CDN (combined with non-image static content).
Average filesize over time, as measured from the CDN (combined with non-image static content).

CDN 上的時間變化與平均文件大小的趨勢圖(包含非圖片的靜態內容)。

咱們沒有作的

這一部分是爲了介紹一些其餘大家可能會用到的改善的方法,Yelp 沒有涉及到是由於咱們選擇的工具鏈以及一些其餘的權衡。

二次抽樣

二次抽樣 是決定網頁圖片質量和文件大小的主要因素。關於二次抽樣的詳細說明能夠在網上找到,可是對於這篇博客簡而言之就是咱們已經使用 4:1:1 二次抽樣過了(通常狀況下 Pillow 的默認設置),因此這裏咱們並不能獲得任何提高。

有損 PNG 編碼

看到咱們對 PNG 的處理以後,你能夠選擇將一部分圖片使用相似 pngmini 的有損編碼器保存爲 PNG,但咱們選擇把圖片另存爲 JPEG 格式。這是另一種不錯的選擇,在用戶沒有修改的狀況下,文件大小就下降了 72-85%。

動態格式

咱們在正在考慮支持更多的新圖片類型,好比 WebP、JPEG2k。即便預約的項目上線了,用戶對於優化過的 JPEG 和 PNG 圖片請求的長尾效應也會繼續發揮做用,使得這一優化仍然是值得的。

SVG

在咱們的網站上不少地方都使用了 SVG,好比咱們的設計師按照風格指導設計的一些靜態資源。這種格式和相似 svgo 這樣的優化工具會顯著減小網頁的負擔,只是和咱們這裏要作的工做沒什麼關係。

供應商的魔力

市面上有不少的供應商能夠提供圖片的傳輸,改變大小,剪裁和轉碼服務。包括開源的 thumbor。或許對咱們來講這是將來支持響應式圖片,動態格式和保留邊框最簡單方法。可是從目前的狀況來看咱們的解決方案已經足夠。

延伸閱讀

下面的這兩本書絕對有他們博客中沒有提到的乾貨,同時也是今天這個主題強烈推薦的延伸閱讀書籍。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索