概述
广告动态创意是指在同样的广告位上,根据触达到的用户的不同属性、行为、场景来展示不同的广告投放素材,达到优化最终该广告的相关点击率、转化率等指标的效果。
动态创意上的文字素材实时生成往往通过文字模板加上预先设定的动态词包来实现,技术原理相对简单;而对图片素材的实时生成则需要在保证请求响应时间的基础上,综合考虑图片的清晰度、大小、编码&渲染成本等因素来选择技术方案,实现难度相对较高。
在线图像合成往往需要保证图片在20ms以内合成完成,现成的开源图像库(ImageMagick, OpenCV等等)往往或者在功能与性能之间倾向某一边,或者不同功能的性能水平参差不齐,并不能完全贴合业务场景需求。
本文深入介绍了动态创意中使用到的图像合成相关技术,以及实际工程实践中总结出的相关经验。先简要介绍图像合成的全流程,然后介绍图像编码的基础知识(以JPEG为主),图像处理中的基本操作,以及如何使用SIMD指令优化性能。
图像合成流程
以上为图像合成的主要流程,简要介绍如下:
图片模板定义了各个组成元素的像素位置、大小、颜色、字体等属性,实践中可以自行设计符合需求的格式,json一般可以满足需求;图片模板中会定义组成元素的布局属性,包括对齐方式、锚点位置、等等,需要换算为Canvas上的绝对坐标值,并提前进行相应的旋转、缩放等操作,会在“图像处理”一节中详细介绍;
图片素材是指组成最终图片的基本素材,例如基本形状(圆形、矩形、箭头等等)、公司logo、动态替换的产品图片,可以使用对象存储系统(例如Ceph)来存储,存储以及利用缓存加速获取速度的细节超出了本文的范畴;图片素材往往是JPEG或者PNG格式,在进入图像处理前需要解压为统一的位图格式,在下一节“图像编码”中会详细介绍;
图层混合则将各个组成元素的位图数据合成为一张最终的位图数据,最后压缩为符合需求的编码格式并将二进制图片文件内容实时返回给终端进行展现。
图像编码
RGB与YUV色彩空间
色彩通常用三个相对独立的属性来描述,按照基本结构可以分两大类:基色色彩空间和色亮分离色彩空间,前者的典型是RGB,由红绿蓝三个分量组成,后者的典型是YUV,由明亮度、色度、浓度三个分量组成。
YUV常使用在视频处理中,从历史来说它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视与黑白电视的兼容问题。具体RGB和YUV之间的转换公式可以参考wiki:https://zh.wikipedia.org/wiki/YUV
YUV格式往往会对UV两个分量进行采样,这是基于人眼对亮度敏感而对色彩不敏感的原理。主流的采样方式有三种:YUV444,YUV422,YUV420。YUV444不做采样,YUV422将每两个像素的UV采样到一个像素,YUV420将每四个像素的UV采样到一个像素。绝大多数图片和视频编码默认采用YUV420采样方式,在8bit色深的情况下,对于每四个像素,YUV444需要3*4=12字节存储空间,而YUV420仅需要4+1+1=6字节存储空间,数据量减半的同时对画质影响不大。下图为采样方式的示例,左边是YUV422,右边是YUV420:
然而在广告动态创意场景下,图片上往往包含许多文字元素,YUV420采样会使文字边缘模糊不清,严重影响效果,因此需要输出YUV444格式的图像。实践中由于大多数使用JPEG编码作为输出格式,UV分量上的采样会增强高频信号从而降低JPEG编码压缩比,因此将采样去除也仅会增加30%左右的最终文件体积。
再来说一说存放格式问题,一般有两种存放格式:packed与planar。前者将每个像素的多个分量放到相邻位置存放,后者将一张图像的多个分量分开存放,以YUV444为例如下:
packed格式无法兼容采样的情况,并且由于内存地址为3字节一组,不方便做SIMD指令优化,因此YUV通常按planar格式存放、计算。而RGB或者RGBA(RGB再加上一个透明度分量)通常更习惯于按packed格式存放。在图层渲染过程中透明度分量的计算是必不可少的,实践中使用packed RGBA格式会同时在性能和工程成本上较优。
JPEG编码
JPEG是一种广泛使用的图片有损压缩编码方法。图像数据中很大一部分是原始采样设备(ccd, cmos)引入的人眼不敏感的噪音,去掉这一部分可以极大程度减少数据量,并且基本不损失图片的基本信息。
JPEG编码一般包括以下步骤:
色彩空间转换:将输入的原始图像数据(RGB,CMYK等等)统一转换到YUV色彩空间;
采样(Downsampling):对UV分量采样,这两步前面均描述过;
离散余弦变换(Discrete cosine transform):将图像分割为8x8的block,并使用DCT变换到频域空间;
量化(Quantization):通过不均匀的量化矩阵来舍弃掉高频信号;
熵编码(Entropy coding):一般是使用Huffman编码。
以上是编码步骤,对应的解码步骤反而行之即可。
DCT与量化
首先说DCT:
这一步先将上一步处理后的图像分割为8x8大小的block,不足8像素的边缘区域用0补齐(因此图片或者视频的尺寸往往对齐到8像素或16像素)。
然后DCT是对每个8x8的block变换到频域下的64个频率系数,这一变换是可逆的。DCT变换的含义可以理解为将原始的8x8 block看做一个64维向量,然后变换到另外一组基上:
左上角(0, 0)位置的分量被称为DC(直流)分量,其余63个系数被称为AC(交流)分量,越靠近右下角频率越高。
然后再说量化,这里使用wiki上给出的例子:https://zh.wikipedia.org/wiki/JPEG
比如有这样一个8x8的block:
DCT之后变为:
然后我们有一个量化表如下:
那么量化其实就是将两个矩阵一一对应相除并取整数,得到:
可以看到量化矩阵对于低频信号(左上角)的量化系数比较低,而对于高频信号(右下角)的量化系数比较高。控制JPEG图像质量其实就是控制量化表,如果我们需要输出一张无损质量的JPEG的话,将量化表的每个格子都填上1即可。下面是两张不同质量度的编码效果对比:
实践中广告素材往往需要80甚至更高的质量度。
量化的逆操作就是将除法改为乘法,但是量化取整过程中舍弃掉的小数部分是不可恢复的,因此对同一张图片进行多次JPEG压缩并且每次都使用不同的量化表会使得量化误差积累出比较大的bias。
Huffman编码
Huffman编码使用变长编码表对源符号进行编码,就是给高概率出现的符号更短的编码,使编码之后的字符串的平均长度降低。JPEG中对于DC和AC的Huffman编码使用不同的编码表与编码方式。
首先来说DC,直观地来看DC分量就是指图像中每个8x8 block的亮度,那么相邻block的DC分量必然有一定相似性,因此JPEG中对DC分量首先做了差分编码,即每个block的DC分量实际上记录的是与上一个block的DC分量的差值,block的顺序按从上到下,从左往右。
再来看AC,每个block有63个AC分量,会按照蛇形(zigzag)顺序进行排放:
这样的顺序会尽可能将大量包含0的高频分量放到一起,然后JPEG会使用0-RLE编码压缩掉相邻的0,例如有这样一组数据:
JPEG中使用RLE压缩后会变为:
每组数据第一个数表示0的重复次数,第二个数用Huffman编码来压缩。关于编码格式更详细的描述可以参考:https://www.impulseadventure.com/photo/jpeg-huffman-coding.html
WebP编码
WebP是2013年Google贡献的一种新的图像编码算法,在相同质量评价(SSIM)下,WebP会比JPEG减少24%~35%,代价是编码耗时会提升10倍以上。下图为WebP官网上对编码流程的介绍:
WebP大致编码流程与JPEG相同:YUV格式转换,划分宏块,DCT,量化,熵编码。其余主要区别如下:
帧内预测
WebP从VP8编码中引入了帧内预测,这一点是JPEG完全不具备的。其原理是将待编码的块减去周围已编码的块得到残差,转而对残差进行编码,从而消除帧内的空域冗余。例如图片中包含大面积蓝天、墙壁等冗余很高的区域时,可以通过帧内预测消除掉相关性。
WebP中主要有如下几种预测模式:
Horizontal:用块左边的一列填充每一列;
Vertical:用块上边的一行填充每一行;
DC:用左边一列与上边一行的平均值填充整个块;
True Motion:类似Horizontal,但填充时考虑块外面左上方像素与块上边一行每个像素的差值。
如下为几种预测模式的示意图:
自适应量化
将图像划分为多个区域,不同的区域采用不同的编码参数(量化表、滤波强度),这样可以对于低细节度的宏块分配更少的bits。自适应量化同样是从VP8编码中引入,VP8对于每一帧最大支持划分成4个区域。
算术编码
WebP使用的熵编码器是算术编码而不是JPEG所使用的Huffman编码,前者的原理是将数据流映射到一个无限位数的小数再转换为二进制,会更接近熵编码所能达到的理论压缩上限,详见:https://zh.wikipedia.org/wiki/%E7%AE%97%E6%9C%AF%E7%BC%96%E7%A0%81
图像处理
本节介绍一些动态创意图像合成中常用的图像处理操作。
透明混合
透明混合(Alpha Blending)指将一张带透明度(Alpha)通道的前景图覆盖(Overlay)到一张背景图上的操作。透明度一般是图片文件中的第4个通道,常见于PNG格式,也可以是用另外一张灰阶图片作为蒙版(Mask)。
用F表示前景图,B表示背景图,α表示透明度,I表示混合结果,那么计算公式为:
α在[0..1]之间,0为完全透明,1为完全不透明。
缩放
图像缩放即将一张原本大小为W x H的图像调整大小为一张大小为W’ x H’的新图像。对于新图像中的任意一个像素(x’, y’),可以计算出对应于原图像中的像素坐标为:
但(x, y)并不是一个整数坐标,如下图所示,围绕(x, y)最近的整数坐标值为x1, x2, y1, y2:
由于(x1, y1), (x2, y2)为1 x 1大小的正方形,可以将(x1, y1)与(x2, y2)平移到(0, 0)与(1, 1)简化计算公式,然后按照双线性插值(Bilinear)计算出新图像中(x’, y’)处像素值为:
类似的图像缩放算法有很多种,区别就是在原图像上采样窗口的大小,以及采样像素点的权重不一样。常用的缩放算法有:
nearest:最近邻插值,即选择原图像中最近的像素;
bilinear:即上面的双线性插值;
bicubic:双三次插值,相比bilinear只使用4个采样点,bicubic使用了16个采样点;
lanczos:该插值算法有一个参数a,a为2或者3的卷积函数如下图所示:
上述几种缩放算法速度越往下越慢,缩放效果越往下越好。实践中需要根据缩放情况是放大(upscale)还是缩小(downscale)分别选择。
模糊
模糊操作是在原始图像上应用一个二维高斯卷积,二维高斯函数定义如下:
理论上计算任意一个像素都需要对原图像每个像素都计算一遍,但实际上距离超过3σ的像素权重已经可以忽略不计。例如下面是一个σ = 0.84089642,大小为7 x 7的卷积核:
直接应用公式计算需要O(n * r^2)的复杂度,n为像素点的数量,r为卷积核大小。工程实现中往往使用多次box blur来近似拟合高斯模糊。
SIMD指令优化
SIMD指令可以在单个CPU指令中同时对多组数据进行运算,运算类型包括:算术运算(加减乘除),位运算,顺序重排,类型转换等等;常用于图像视频编解码、数据分析等容易并行的计算任务。例如将两个uint32数组相加,利用SSE指令集可以将每4个uint32一组放进XMM寄存器同时计算,大大提高执行效率。
在广告动态创意的图像合成中,由于一般使用RGBA格式存储图像,每个像素占用4字节,那么使用128位的SIMD指令可以一次操作4个像素,大大提高了图像处理效率。
SIMD简介
下图为Intel CPU上SIMD指令集的发展历程:
目前最常用的为128位的SSE指令集,包括了SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2指令集。使用256或者512位的AVX指令集可以进一步提高执行效率,但到目前为止实际生产环境仍有大量不支持AVX2指令集的CPU,而AVX指令集仅支持浮点运算。考虑实际运行环境的CPU兼容性问题,使用SIMD指令的软件往往需要针对同一功能编写多个版本的代码以在不同level的CPU上都能达到性能最优,本文这里以SSE为例进行介绍。
使用SSE指令集
使用SSE指令集有两种方式:Assembly(内嵌汇编),Intrinsic(编译器内部函数)。Assembly的方式即直接编写汇编代码,Intrinsic的方式为调用编译器提供的函数,但编译的阶段会被直接转换为SSE指令而不是函数调用。
Assembly方式为手工编写汇编代码,因此通过指令重排、循环展开等各种优化可以获得超过Intrinsic的性能;但开发成本也会相应提高
,因为不同的指令性能不一(吞吐、延迟),即使是相同的指令在不同CPU架构下也会有相对性能差异;因此下面主要介绍较为简单的Intrinsic方式。
如下为一段使用SSE指令的代码,将RGBA格式的4个像素转换为RGB格式:
第一行先引用Intrinsic头文件;
然后__m128i为SSE寄存器类型,实际编译过程中会被替换成XMM0~XMM7中的某个寄存器;
_mm_setr_epi8()使用16个byte初始化了一个XMM寄存器;
_mm_loadu_si128()和_mm_storeu_si128()分别是将内存加载到XMM寄存器和将XMM寄存器写回内存;
_mm_shuffle_epi8()则完成了对XMM寄存器的byte粒度顺序重排。
这段代码所起的作用是将”RGBARGBARGBARGBA”所表示的4个像素转换成了”RGBRGBRGBRGB__“,效率会比C版本提高5倍左右。SSE版本的其它图像操作普遍会比C版本提高2~8倍效率。
总结
本文介绍了广告动态创意中在线图像合成的若干关键技术点,对图像合成的性能起到至关重要的作用。除此之外还有一些可以进一步提升性能的手段,例如:
使用GPU来进行图像处理(图片解压、合成)可以进一步提高图像合成性能,但服务器成本也会大幅提高;
使用JPEG无损操作可以省去一部分合成、压缩的时间,需要对JPEG编码库进行二次开发,原理是将没有修改过的原始block的压缩数据流直接剪切到合成结果图片的压缩数据流中间去,具体可以参考:http://jpegclub.org/jpegtran/
领取专属 10元无门槛券
私享最新 技术干货