这篇教程,是系列教程的第三篇,前篇名为iOS平台图片编解码入门教程(第三方编解码篇)。由于vImage已经属于较为底层框架,这一篇将不会特别着重图片封装格式的编解码,会介绍一些Bitmap级别的操作,包括了图像的色彩转换,Alpha合成、基本几何变换等实际用法。由于教程侧重是图像格式,所以不会介绍vImage强大的Convolution等知识,这方面涉及到数字图像处理的复杂知识,不是教程的目标
基本介绍 vImage 是Apple的Accelerate库的一部分,侧重于高性能的图像Bitmap级别的处理。库本身全部是C的接口,而且不同于Core系列的(Core Graphics/Core Foundation)C接口,是比较贴近传统C语言的接口,不会有XXXRef这种贴心的定义,而且很多接口需要自己手动分配内存。
vImage按照功能,可以分为Alpha Compositing(Alpha合成)、Geometry(几何变换)、Conversion(色彩转换)、Convolution(卷积,用于图像滤镜)Morphology(形态学处理)等。这里主要介绍的,就是色彩转换,Alpha合成,以及几何变换的内容。
首先需要对vImage的基本接口有所了解,有这么几个概念:
vImage_Buffer
: 对应Bitmap的数据,只有最基本的width、height、rowBytes(stride)以及data
vImage_CGImageFormat
: 每个vImage的功能,会提供不同色彩格式的类似接口,比如会有ARGB8888,Planar8的同样功能。这里ARGB8888指的是ARGB排列,每通道占8个Bit,也就是一个Piexel占32Bit。而vImage还有一个常见的色彩格式Plane8,指的是只有一个通道(平面),按照顺序排列,比如{R, R, R, R}
这样,更方便进行计算
vImage_Flags
: 每个vImage接口,都会有一个flags
参数来控制一些选项,比如说可以自己定义内存分配,背景色填充策略,重采样策略等,默认的是kvImageNoFlags
vImage_Error
: 每个vImage的接口,都会返回这个result,来让用户确认是否成功,以及失败的原因,在Debug下比较有帮助
为了统一期间,以下的内容,都是基于ARGB8888色彩格式的输入来说明的。其他的情况处理,参考同名接口的不同格式即可。
色彩转换 色彩转换指的是将图像的Bitmap格式,从一个色彩格式,比如ARGB8888,转换到另一个色彩格式,比如说RGB888的功能。对于RGB来说,一般来说就是通道的增加和减少。当然还有RGB转为Planar8的情况。
vImage对这些色彩转换的功能,统一提供了方法vImageConvert_AtoB
,比如ARGB8888转RGB888,就可以用下面的代码来处理。顺便通过这个代码,来简单了解vImage的API的基本用法。
先来定义几个简单的结构体,方便后续使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static vImage_CGImageFormat vImageFormatARGB8888 = (vImage_CGImageFormat) { .bitsPerComponent = 8 , .bitsPerPixel = 32 , .colorSpace = NULL , .bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrderDefault, .version = 0 , .decode = NULL , .renderingIntent = kCGRenderingIntentDefault, }; static vImage_CGImageFormat vImageFormatRGB888 = (vImage_CGImageFormat) { .bitsPerComponent = 8 , .bitsPerPixel = 24 , .colorSpace = NULL , .bitmapInfo = kCGImageAlphaNone | kCGBitmapByteOrderDefault, .version = 0 , .decode = NULL , .renderingIntent = kCGRenderingIntentDefault, }; static inline size_t vImageByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1 )) / alignment) * alignment; }
接着,就是完整的转换代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 + (CGImageRef )nonAlphaImageWithImage:(CGImageRef )aImage { __block vImage_Buffer a_buffer = {}, output_buffer = {}; @onExit { if (a_buffer.data) free(a_buffer.data); if (output_buffer.data) free(output_buffer.data); }; vImage_Error a_ret = vImageBuffer_InitWithCGImage(&a_buffer, &vImageFormatARGB8888, NULL , aImage, kvImageNoFlags); if (a_ret != kvImageNoError) return NULL ; output_buffer.width = a_buffer.width; output_buffer.height = a_buffer.height; output_buffer.rowBytes = vImageByteAlign(output_buffer.width * 3 , 64 ); output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); vImage_Error ret = vImageConvert_ARGB8888toRGB888(&a_buffer, &output_buffer, kvImageNoFlags); if (ret != kvImageNoError) return NULL ; CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &vImageFormatRGB888, NULL , NULL , kvImageNoFlags, &ret); if (ret != kvImageNoError) return NULL ; return outputImage; }
任意色彩格式转换 除了一系列vImageConvert_AtoB
的转换,vImage还提供了一个非常抽象的接口,叫做vImageConvert_AnyToAny
,只需要你提供一个input format,一个output format,就可以直接转换。这个接口比较强大,不仅能够handler所有支持的色彩格式,而且还能支持CVImageBuffer
(通过这个vImageConverter
来构造)。所以一般如果做库封装,做一些色彩转换的case的时候,就可以试着用这个接口。
因此,我们之前的ARGB8888ToRGB888的色彩转换,可以这样写,更为通用。示例代码:
1 2 3 vImageConverterRef converter = vImageConverter_CreateWithCGImageFormat(&vImageFormatARGB8888, &vImageFormatRGB888, NULL , kvImageNoFlags, &ret); if (ret != kvImageNoError) return NULL ;ret = vImageConvert_AnyToAny(converter, &a_buffer, &output_buffer, NULL , kvImageNoFlags);
Alpha合成
Alpha合成 指的是将两张含有Alpha通道的图(被Blend的叫做bottom,Blend的叫做top),通过一定的公式合成成为一张新的含Alpha通道的图,一般来说用于给图像添加遮罩、覆盖等,常见的图像处理软件都有这个功能。其实本质上来说,Alpha合成,就是对图像的每一个像素值,进行这样一个计算:
1 2 3 4 5 resultAlpha = (topAlpha * 255 + (255 - topAlpha) * bottomAlpha + 127 ) / 255 resultColor = (topAlpha * topColor + (((255 - topAlpha) * bottomAlpha + 127 ) / 255 ) * bottomColor + 127 ) / resultAlpha
公式看起来比较复杂,因此这里顺便可以介绍一下关于premultiplied-alpha 的概念,直观地说,就是将(r, g, b, a)
预先乘以了对应的alpha通道的值,成为(r * a, g * a, b * a, a)
。这个带来的好处,就是Alpha合成的时候,可以少一次乘法,而且简化了计算,成为这样子:
1 2 resultColor = (topColor + (((255 - topAlpha) * bottomAlpha + 127 ) / 255 ) * bottomColor + 127 )
在vImage中,已经提供了一个接口来专门处理Alpha合成,针对nonpremultiplied的,是vImageAlphaBlend_ARGB8888
,而针对premultiplied,是vImagePremultipliedAlphaBlend_ARGB8888
。需要注意的是,这个接口要求的两个buffer,宽度和高度必须相等,因此,我们对于Color和Image的遮罩,需要进行处理,保证这两个buffer满足要求。
Alpha Blend Color 这个用处,一般是用来做图像的遮罩的,可以对图像整体盖一层有透明度的颜色,比如说夜间模式,纯色滤镜等。根据上面说的,如果需要对一个Bitmap使用vImage进行Alpha Blend,我们需要保证两个buffer的宽度和高度相同,因此可以使用vImageBufferFill_ARGB8888
填充整个Color来构造一个与输入图像Buffer相同宽高的新buffer,然后用它来进行Alpha Blend。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CGImageRef aImage; CGColorRef color; __block vImage_Buffer a_buffer = {}, b_buffer = {}, output_buffer = {}; Pixel_8888 pixel_color = {0 }; const double *components = CGColorGetComponents (color);const size_t components_size = CGColorGetNumberOfComponents (color);if (components_size == 2 ) { pixel_color[0 ] = components[1 ] * 255 ; } else { pixel_color[0 ] = components_size == 3 ? 255 : components[3 ] * 255 ; pixel_color[1 ] = components[0 ] * 255 ; pixel_color[2 ] = components[1 ] * 255 ; pixel_color[3 ] = components[2 ] * 255 ; } vImage_Error b_ret = vImageBufferFill_ARGB8888(&b_buffer, pixel_color , kvImageNoFlags); if (b_ret != kvImageNoError) return NULL ;vImage_Error ret = vImageAlphaBlend_ARGB8888(&b_buffer, &a_buffer, &output_buffer, kvImageNoFlags);
Alpha Blend Image 上面说到了关于Color的Alpha Blend,不同于Color这种需要填充全部宽度,如果对于一个Image需要进行Alpha Blend,我们大部分情况都是需要制定一个起始点的,因为不能保证所有输入的两个Image的宽高相同。因此设计的时候,可以给用户提供一个point参数,以这个坐标点开始来绘制Alpha Blend,类似于很多图像编辑软件提供的图层功能。
由于vImage的Alpha Blend需要两个等宽高的Buffer,因此我们需要对用户提供的Top Image进行处理,通过平移变换移动到指定的Point以后,填充其余部分为Clear Color。最后进行Alpha Blend即可。
1 2 3 4 5 6 7 8 9 10 11 12 CGImageRef aImage, bImage; __block vImage_Buffer a_buffer = {}, b_buffer = {}, c_buffer = {}, output_buffer = {}; CGAffineTransform transform = CGAffineTransformMakeTranslation (point.x, point.y);vImage_CGAffineTransform cg_transform = *((vImage_CGAffineTransform *)&transform); Pixel_8888 clear_color = {0 }; vImage_Error c_ret = vImageAffineWarpCG_ARGB8888(&b_buffer, &c_buffer, NULL , &cg_transform, clear_color, kvImageBackgroundColorFill); if (c_ret != kvImageNoError) return NULL ;vImage_Error ret = vImageAlphaBlend_ARGB8888(&c_buffer, &a_buffer, &output_buffer, kvImageNoFlags);
几何变换 几何变换,指的是将一个原始的Bitmap,通过线性方法进行处理,实现比如平移、缩放、旋转、错切等操作的图像处理技术。
可能大部分人已经知道了(之前也说过),Core Graphics的坐标系统,和UIKit的坐标系统,在Y坐标上是相反的。UIKit的使用的是Y轴正向垂直向下的左手系,而Core Graphics和普通的右手系直角坐标系相同。vImage也遵守了右手系,因此之后介绍的变换都是按照右手系的,如果想处理UIKit的坐标系,自己转换一下即可(一般就是取image.height - offsetY
即可)
关于要介绍的的这些几何变换,虽然都最后可以统一到到线性变换上,只不过效率上可能相比单独的方法来说有所损耗,因此单独对每个功能所需要的vImage接口进行了介绍。关于线性变换不太理解的,可以参考一下之前的一篇教程:Core Graphics仿射变换知识
缩放
缩放是最简单的一个处理过程,但是由于缩放之后,之前的同一个像素点,现在可能会映射到4个或者更多像素点,或者是原本4个像素点,现在需要映射到1个像素点。这就会涉及到一个叫做图像重采样 的过程。具体来说,就是对每一个像素,所在的Bitmap的子矩阵(比如3x3),通过一定的算法计算,得到对应的缩放以后的中心像素的值。同时,这个像素值可能变成浮点数,还需要进行处理,最后填到采样后的Bitmap相应的位置上。常见的简单处理有最邻近算法、双线性算法、双立方算法等。
vImage默认使用的是Lanczos Algorithm ,具体的介绍可以参考Wikipedia和DSP相关的书籍。这里有一个直观的对比表现网页 。如果想要更高画质的算法,可以提供kvImageHighQualityResampling
参数,来使用Lanczos5
算法。或者可以使用之后要谈的相对底层一点的错切API,来自定义你的重采样过程。
vImage提供了自带的vImageScale_ARGB8888
方法,这里就简单举个例子(之前重复代码的都略过):
1 2 3 4 5 6 7 8 CGSize size; output_buffer.width = MAX(size.width, 0 ); output_buffer.height = MAX(size.height, 0 ); output_buffer.rowBytes = vImageByteAlign(output_buffer.width * 4 , 64 ); output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); if (!output_buffer.data) return NULL ;vImage_Error ret = vImageScale_ARGB8888(&a_buffer, &output_buffer, NULL , kvImageHighQualityResampling);
裁剪 裁剪是指的将原始Bitmap,只裁出来指定矩形大小的部分,其余部分直接丢弃的过程。虽然vImage没有提供直接的API来处理这个流程(当然你是可以用vecLib的方法,直接对Bitmap进行矩阵操作,但是有点过于小题大做了)。但是实际上,这就是一个平移变换能够搞定的事情。我们只需要对输入目标的坐标的CGRect
进行转换,将原始图像平移之后,再限制输出的Bitmap的大小,这样平移超出部分就会自动被裁掉。不需额外的处理,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 CGRect rect; output_buffer.width = MAX(CGRectGetWidth (rect), 0 ); output_buffer.height = MAX(CGRectGetHeight (rect), 0 ); output_buffer.rowBytes = vImageByteAlign(output_buffer.width * 4 , 64 ); output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); if (!output_buffer.data) return NULL ;CGFloat tx = CGRectGetMinX (rect);CGFloat ty = CGRectGetMinY (rect);CGAffineTransform transform = CGAffineTransformMakeTranslation (-tx, -ty);vImage_CGAffineTransform cg_transform = *((vImage_CGAffineTransform *)&transform); Pixel_8888 clear_color = {0 }; vImage_Error ret = vImageAffineWarpCG_ARGB8888(&a_buffer, &output_buffer, NULL , &cg_transform, clear_color, kvImageBackgroundColorFill);
镜像 镜像顾名思义,就是将图像沿着某个轴进行翻转,比如沿X轴就是水平镜像,同一个像素点,对应的X坐标不变,Y坐标变为高度减去本身的Y坐标即可。
vImage对应的API,是vImageVerticalReflect_ARGB8888
和vImageHorizontalReflect_ARGB8888
,使用起来也比较简单。直接上一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 BOOL horizontal;__block vImage_Buffer a_buffer = {}, output_buffer = {}; vImage_Error ret; if (horizontal) { ret = vImageHorizontalReflect_ARGB8888(&a_buffer, &output_buffer, kvImageHighQualityResampling); } else { ret = vImageVerticalReflect_ARGB8888(&a_buffer, &output_buffer, kvImageHighQualityResampling); }
旋转 旋转也是非常常见一个图像几何几何变化。具体坐标的变化就是对旋转的角度,求对应三角函数到X轴和Y轴的投影结果,比较直观。
vImage对旋转也提供了一个非常方便的API,角度是弧度值,按照顺时针方向进行。另外,由于输出的Buffer的大小会限制图像大小,而旋转后可能超出原图大小,我们需要对输出的大小也计算出对应的新的大小。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 CGFloat radians; CGSize size = CGSizeMake (a_buffer.width, a_buffer.height);CGAffineTransform transform = CGAffineTransformMakeRotation (radians);size = CGSizeApplyAffineTransform (size, transform); output_buffer.width = ABS(size.width); output_buffer.height = ABS(size.height); output_buffer.rowBytes = vImageByteAlign(output_buffer.width * 4 , 64 ); output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); if (!output_buffer.data) return NULL ;Pixel_8888 clear_color = {0 }; vImage_Error ret = vImageRotate_ARGB8888(&a_buffer, &output_buffer, NULL , radians, clear_color, kvImageBackgroundColorFill | kvImageHighQualityResampling);
错切
错切 是一种特殊的线性变换,直观的介绍可以从Wikipedia上看,也可以参考之前的另一篇教程。主要的参数有一个m值,表示对应参考坐标的缩放倍数。
在vImage中,错切变换是相对底层的接口,实际上,线性变换是通过这三个接口(错切、旋转、镜像)来实现的。错切的接口,比如水平错切对应的是vImageHorizontalShear_ARGB8888
,参数算是最多的一个,稍微详细介绍一下:
srcOffsetToROI_X
: 错切定位点水平偏移量,具体指的就是左上角那个像素点,在经过旋转的映射后,水平偏移的距离,会影响最后图像(除去Buffer的宽度限制)的整体宽度
srcOffsetToROI_Y
: 错切定位点的垂直偏移量,类似水平值
xTranslate
: 错切完成后的水平平移距离
shearSlope
: 错切的弧度值,顺时针
filter
: 用来自定义重采样的方法,一般用自带的vImageNewResamplingFilter
,或者也可以提供一个函数指针构造对应的重采样过程。会用到一个scale参数,表示这个重采样对应的缩放倍数,也就是错切的m值
backgroundColor
: 背景填充色
对应的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CGVector offset; CGFloat translation; CGFloat slope; CGFloat scale; output_buffer.width = MAX(a_buffer.width - offset.dx, 0 ); output_buffer.height = MAX(a_buffer.height - offset.dy, 0 ); output_buffer.rowBytes = vImageByteAlign(output_buffer.width * 4 , 64 ); output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); if (!output_buffer.data) return NULL ;Pixel_8888 clear_color = {0 }; ResamplingFilter resampling_filter = vImageNewResamplingFilter(scale, kvImageHighQualityResampling); vImage_Error ret; if (horizontal) { ret = vImageHorizontalShear_ARGB8888(&a_buffer, &output_buffer, offset.dx, offset.dy, translation, slope, resampling_filter, clear_color, kvImageBackgroundColorFill); } else { ret = vImageVerticalShear_ARGB8888(&a_buffer, &output_buffer, offset.dx, offset.dy, translation, slope, resampling_filter, clear_color, kvImageBackgroundColorFill); } vImageDestroyResamplingFilter(resampling_filter);
线性变换 最后再来说通用的线性变换吧,这个其实在之前的功能中已经用到过了,vImage有兼容Core Graphics的CGAffineTransform
的结构体vImage_CGAffineTransform
,两个结构体对应的内存布局是一样的,直接强制转换过去就可以了,不需要单独赋一遍。关于通用线性变换的内容就不再赘述了,有兴趣可以查看相关资料,或者之前的教程:Core Graphics仿射变换知识
示例代码:
1 2 3 4 5 CGAffineTransform transform; vImage_CGAffineTransform cg_transform = *((vImage_CGAffineTransform *)&transform); Pixel_8888 clear_color = {0 }; vImage_Error ret = vImageAffineWarpCG_ARGB8888(&a_buffer, &output_buffer, NULL , &cg_transform, clear_color, kvImageBackgroundColorFill | kvImageHighQualityResampling);
总结 vImage是一个比较底层的图像Bitmap处理的库,在这里介绍了关于色彩转换、Alpha合成、几何变换等基本知识。相比于简单的Core Graphics的处理,能够提供更为复杂的参数控制,并且带来较高的性能。对于很多图像密集处理软件处理来说,用Core Graphics显的比较低效,因此可以考虑vImage。
但是vImage强大之处远不在这里,里面还包含了类似图像卷积,形态处理等,可以对复杂滤镜进行支持,类似于GPUImage 。这些功能都需要数字图像处理相关知识,在这种教程系列就不会介绍了。
对于这篇教程的示例代码,其实我写了个非常简单的库,放到GitHub上了:vImageProcessor ,有兴趣的可以去参考一下,希望能够用于自己的图片处理相关框架中。
由于自己完全是业余兴趣,工作和图像处理基本不相关,并不打算深入学习数字图像处理的知识,因此这个教程可能就会暂时告一段落了。最后,之所以写这篇教程,是因为自己想要参考一下vImage的教程,却发现只会搜出来一堆互相抄袭的内容,而且大部分都是关于图像滤镜的,对于图像处理本身不会太多介绍。我希望这系列教程,能给同样对图像编解码、图像处理有一点兴趣的人,提供一个相对简单且清晰的入门概览吧。