- 原文地址:Making Photos Smaller Without Quality Loss
- 原文做者:Stephen Arthur
- 译文出自:掘金翻译计划
- 译者:Xat_MassacrE
- 校对者:meifans,windmxf
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
这是咱们作出的最简单的改变之一:开启 Pillow 中负责以 CPU 耗时为代价节省额外的文件大小的设置 (optimize=True
)。因为本质没变,全部这对于图片质量丝毫没有影响。android
对于 JPEG 来讲,对个选项告诉编码器经过对每一个图片进行一次额外的扫描以找到最佳的 霍夫曼编码。第一次,不写入文件,而是计算每一个值出现的次数,以及能够计算出理想编码的必要信息。PNG 内部使用 zlib,因此在这种状况下优化选项告诉编码器使用 gzip -9
而不是 gzip -6
。ios
这是一个很简单的改变,可是事实证实它也不是银弹,由于文件大小只减小了百分之几。git
当咱们将一张图片保存为 JPEG 时,你能够从下面的选项中选择不一样的类型:github
progressive=True
)。这是一个能明显感受到的性能提高(就是比起不是清晰的图片,只加载一半的图片更容易注意到。)还有就是渐进式文件的被打包时会有一个小幅的压缩。更详细的解释请看 Wikipedia article,JPEG 格式在 8x8 像素块上使用锯齿模式进行熵编码。当这些像素块的值被解压并按顺序展开时,你会发现一般状况下非零的数字会优先出现,而后是零的序列,那个模式会对图片的每个 8x8 的像素块进行隔行扫描。使用渐进编码时,被解压开的像素块的顺序会逐渐改变。每一个块中较大的值将会在文件中首先出现,(渐进模式加载的图片中区分度最高的区域将最先被扫描),而一段较长的小数字,包括许多数字零,将会在最末加载,用于填充细节。这种图片数据的从新排列不会改变图片自己,可是确实可能在某一行(这一行能够被更容易的压缩)中增长了 0 的数量。算法
一个美味的甜甜圈的图片的对比(点击放大):ubuntu
模拟标准 JPEG 图片的渲染效果。
模拟渐进式 JPEG 图片的渲染效果。
Yelp 为用户上传的图片主要提供两种格式 - JPEG 和 PNG。JPEG 对于照片来讲是一个很棒的格式,可是对于高对比度的设计内容,相似 logo,就不那么优秀了。而 PNG 则是彻底无损的,因此很是适用于图形类型的图片,可是对于差别不明显的图片又显得太大了。若是用户上传的 PNG 图片是照片的话(经过咱们的识别),使用 JPEG 格式来存储就会节省很大的空间。一般状况下,Yelp 上的 PNG 图片都是移动设备和 "美图类" app 的截图。
(左边) 一张明显的 PNG 合成图。(右边) 一张明显的 PNG 的截图。
咱们想减小这些没必要要的 PNG 图片的数量,但重要的是要避免过分干预,改变格式或者下降图片质量。那么,咱们如何来识别一张图片呢?经过像素吗?
经过一组 2500 张图片的实验样本,咱们发现文件大小和独立像素结合起来能够很好地帮助咱们判断。咱们在最大分辨率下生成咱们的候选缩略图,而后看看输出的 PNG 文件是否大于 300KB。若是是,咱们就检测图片内容是否有超过 2^16 个独立像素(Yelp 会将 RGBA 图片转化为 RGB,即便不转,咱们也会作这个检测)。
在实验数据集中,手动调整定义大图片的数值能够减小 88% 的文件大小(也就是说,若是咱们将全部的图片都转换的话,咱们预期能够节约的存储空间),而且这些调整对图片是无损的。
第一个也是最广为人知的减少 JPEG 文件大小的方法就是设置 quality
。不少应用保存 JPEG 时都会设置一个特定的质量数值。
质量实际上是个很抽象的概念。实际上,一张 JPEG 图片的每一个颜色通道都有不一样的质量。质量等级从 0 到 100 在不一样的颜色通道上都对应不一样的量化表,同时也决定了有多少信息会丢失。
在信号域量化是 JPEG 编码中失去信息的第一个步骤。
减小文件大小最简单的方法其实就是下降图片的质量,引入更多的噪点。可是在给定的质量等级下,不是每张图片都会丢失一样多的信息。
咱们能够动态地为每一张图片设置最优的质量等级,在质量和文件大小之间找到一个平衡点。咱们有如下两种方法能够作到这点:
咱们评估了一个 bottom-up 算法,可是到目前为止,这个算法尚未在咱们的实验环境下获得一个满意的结果(虽然这个算法看上去在中等质量图片地处理上还有很多发展潜力,由于处理中等质量图片能够丢弃更多的信息)。不少关于这个算法的 学术论文 在 90 年代早期发表,可是在这个算力昂贵的时代,bottom-up 算法的实现走了捷径,好比没有评估像素块之间的相互影响。
因此咱们选择第二种方法:使用二分法在不一样的质量等级下生成候选图片,而后使用 pyssim 计算它的结构类似矩阵 (SSIM) 来评估每张候选图片损失的质量,直到这个值达到非静态可配置的阈值为止。这个方法让咱们能够有选择地下降文件大小(和文件质量),可是只适用于那些即便下降质量用户也察觉不到的图片。
在下面的图表中,咱们画出了经过 3 个不一样的质量等级生成的 2500 张图片的 SSIM 值的图像。
quality = 85
生成的原始图。quality = 80
生成的图。SSIM 80-85
。为一张图片基于汇合点或者超过 SSIM 比率(一个提早计算好的静态值,使得转换发生在图像范围中间的某处)的地方在 80 到 85 (包括 85) 之间选择一个质量值。这种方法能够有效地减少图片大小,可是又不会突破咱们图片质量要求的底线。2500 张 3 种不一样的质量策略的 SSIM 值。
这里有很多能够模拟人类视觉系统的图片质量算法。在评估了不少方法以后,咱们认为 SSIM 这个方法虽然比较古老,但倒是最适合对这几个特征作迭代优化的:
动态质量的实例代码:
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 也发表过一篇!快去看看吧!
Mozjpeg 是 libjpeg-turbo 的一个开源分支,是经过执行时间来置换文件的大小的编码器。这种方法完美的契合离线批处理再生成图片。在比 libjpeg-turbo 多投入 3 到 5 倍的时间,和一点复杂的算法就可使图片变的更小了!
mozjpeg 这个编码器最大的不一样点就是使用了一张额外的量化表。就像上面提到的,质量是每个颜色通道量化表的一个抽象的概念。默认 JPEG 量化表的全部信号点都十分容易被命中。用 JPEG 指导 中的话说就是:
这些表仅供参考,不能保证在任何应用中都是适用的。
因此说,大部分编码器的实现默认状况下使用这些表就不足为奇了。
Mozipeg已经替咱们扫平了使用基准测试选择表的麻烦,并使用性能最好的通用替代方案建立图片。
大部分 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 张图片并使用咱们的工做流来处理,看看文件大小都有什么变化:
这些所有加起来可让图片大小平均减少大概 30%,而且咱们应用在最大最多见分辨率的图片上,对于用户来讲,不只咱们的网页变的更快,同时平均天天还能够节省兆兆字节的数据传输量。从 CDN 上就可见一斑:
CDN 上的时间变化与平均文件大小的趋势图(包含非图片的静态内容)。
这一部分是为了介绍一些其余大家可能会用到的改善的方法,Yelp 没有涉及到是由于咱们选择的工具链以及一些其余的权衡。
二次抽样 是决定网页图片质量和文件大小的主要因素。关于二次抽样的详细说明能够在网上找到,可是对于这篇博客简而言之就是咱们已经使用 4:1:1
二次抽样过了(通常状况下 Pillow 的默认设置),因此这里咱们并不能获得任何提高。
看到咱们对 PNG 的处理以后,你能够选择将一部分图片使用相似 pngmini 的有损编码器保存为 PNG,但咱们选择把图片另存为 JPEG 格式。这是另一种不错的选择,在用户没有修改的状况下,文件大小就下降了 72-85%。
咱们在正在考虑支持更多的新图片类型,好比 WebP、JPEG2k。即便预约的项目上线了,用户对于优化过的 JPEG 和 PNG 图片请求的长尾效应也会继续发挥做用,使得这一优化仍然是值得的。
在咱们的网站上不少地方都使用了 SVG,好比咱们的设计师按照风格指导设计的一些静态资源。这种格式和相似 svgo 这样的优化工具会显著减小网页的负担,只是和咱们这里要作的工做没什么关系。
市面上有不少的供应商能够提供图片的传输,改变大小,剪裁和转码服务。包括开源的 thumbor。或许对咱们来讲这是将来支持响应式图片,动态格式和保留边框最简单方法。可是从目前的状况来看咱们的解决方案已经足够。
下面的这两本书绝对有他们博客中没有提到的干货,同时也是今天这个主题强烈推荐的延伸阅读书籍。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。