iOS8 Core Image In Swift:自动改善图像以及内置滤镜的使用
上面那篇文章主要是Core Image的基础,只是为了说明CIImage、CIFilter、CIContext,以及基础滤镜的简单使用。在上一篇中几乎没有对滤镜进行更复杂的操作,都是直接把inputImage扔给CIFilter而已,而Core Image实际上还能对滤镜进行更加细粒度的控制,我们在新的工程中对其进行探索。为此,我重新建立了一个空的workspace,并把之前所使用的工程添加到这个workspace中,编译、运行,没问题的话我们就开始创建新的工程。
通过workspace左下角的Add Files to添加已有的工程:
当添加工程到workspace的时候,记得要把被添加的工程关掉,不然workspacce不能识别。 另外,在流程上这篇也会与上一篇不同,上一篇一开始我就给出了代码,然后先看效果再步步为营,这篇不会在一开始给出代码。 |
动态改变滤镜参数的值
用Single View Application的工程模板建立一个新的工程,在View上放一个UIImageView,还是同样的frame,同样的ContentMode设置为Aspect Fit,同样的关闭Auto Layout以及Size Classes,最后把上个工程中使用的图片复制过来,在这个工程中同样使用这张图。
做完上面这些基础工作后,我们回到VC中,把showFiltersInConsole方法从上个工程中复制过来,然后在viewDidLoad里调用,在运行之前我们先看看Core Image有哪些类别,毕竟全部的滤镜有127种,不可能一一用到的。
类别有很多,而且我们从上一篇中知道了滤镜可以同时属于不同的类别,除此之外,类别还分为两大类:
按效果分类:
- kCICategoryDistortionEffect 扭曲效果,比如bump、旋转、hole
- kCICategoryGeometryAdjustment 几何开着调整,比如仿射变换、平切、透视转换
- kCICategoryCompositeOperation 合并,比如源覆盖(source over)、最小化、源在顶(source atop)、色彩混合模式
- kCICategoryHalftoneEffect Halftone效果,比如screen、line screen、hatched
- kCICategoryColorAdjustment 色彩调整,比如伽马调整、白点调整、曝光
- kCICategoryColorEffect 色彩效果,比如色调调整、posterize
- kCICategoryTransition 图像间转换,比如dissolve、disintegrate with mask、swipe
- kCICategoryTileEffect 瓦片效果,比如parallelogram、triangle
- kCICategoryGenerator 图像生成器,比如stripes、constant color、checkerboard
- kCICategoryGradient 渐变,比如轴向渐变、仿射渐变、高斯渐变
- kCICategoryStylize 风格化,比如像素化、水晶化
- kCICategorySharpen 锐化、发光
- kCICategoryBlur 模糊,比如高斯模糊、焦点模糊、运动模糊
按使用场景分类:
- kCICategoryStillImage 能用于静态图像
- kCICategoryVideo 能用于视频
- kCICategoryInterlaced 能用于交错图像
- kCICategoryNonSquarePixels 能用于非矩形像素
- kCICategoryHighDynamicRange 能用于HDR
这些专业词太难翻译了,有不准确的地方还望告知 |
此外还有我们之前用到的kCICategoryBuiltIn。
我们把kCICategoryColorAdjustment这个类别下的滤镜打印出来看看:
有11个滤镜,其中有一个CIHueAdjust,这个看名字应该是修改图像色调的,效果应该会比较明显,看看它有哪些参数:
它的详细信息里除了我们之前了解的inputImage和所属分类信息以外,多了个inputAngle,显然这是一个输入参数,而且这个参数也打印的非常清晰,其中包括了:
- 参数类型:NSNumber
- 默认值:0
- kCIAttributeIdentity:虽然这个值大部分情况下与默认值是一样的,但是它们的含义不一样,kCIAttributeIdentity表示的含义是这个值被应用到参数上的时候,就表示被应用的参数不会对inputImage造成任何影响
- 最大值:?
- 最小值:-?
- 属性类型:角度
上面的这些参数以及取值对不同的CIFilter来说都不一样,要具体情况具体分析。
了解了以上情况后,我们就可以开始编码了。首先在VC里添加上个工程中的常用属性:
class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView!
@IBOutlet var slider: UISlider!
lazy var originalImage: UIImage = {
return UIImage(named: "Image")
}()
lazy var context: CIContext = {
return CIContext(options: nil)
}()
var filter: CIFilter!
......
与之前工程中不同的是,我多加了一个UISlider,Main.storyboard中VC的view像这样:
把UIImageView及UISlider的连线与VC中的连接起来,然后我们在viewDidLoad方法里写上:
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.shadowOpacity = 0.8
imageView.layer.shadowColor = UIColor.blackColor().CGColor
imageView.layer.shadowOffset = CGSize(width: 1, height: 1)
slider.maximumValue = Float(M_PI)
slider.minimumValue = Float(-M_PI)
slider.value = 0
slider.addTarget(self, action: "valueChanged", forControlEvents: UIControlEvents.ValueChanged)
let inputImage = CIImage(image: originalImage)
filter = CIFilter(name: "CIHueAdjust")
filter.setValue(inputImage, forKey: kCIInputImageKey)
slider.sendActionsForControlEvents(UIControlEvents.ValueChanged)
showFiltersInConsole()
}
imageView的设置同以前一样,增加点阴影显得好看多了。
接着对slider初始化,在之前我们了解到CIHueAdjust滤镜的inputAngle参数最大值是?,最小值是负?,默认值是0,就用这些值来初始化,然后添加一个当值发生改变时触发的事件。
初始化filter,由于只有一个滤镜,filter对象也可以重用,设置完inputImage后,触发slider的事件就可以了。
valueChanged方法实现:
@IBAction func valueChanged() {
filter.setValue(slider.value, forKey: kCIInputAngleKey)
let outputImage = filter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
filter会在每次触发这个事件的时候更新inputAngle属性,同时输出到imageView上。
虽然我并不是在Storyboard里把slider的valueChanged事件连接到VC的方法上,但是在这里使用@IBAction也是适当的,这样可以表明这个方法不是业务逻辑方法,而是一个UI控件触发的方法。
编译、运行,应该可以看到效果了。
复合滤镜--老电影效果
在此之前,无论是使用简单滤镜,还是能动态修改参数值的滤镜,都不算复杂,因为我们最多也只是对一个滤镜设置点参数而已。可是如果现有的滤镜没有想要的效果,或者说单个滤镜实现不了自己想要的效果,就只能自己处理了,其中,最简单的做法是把多个滤镜组合起来。
Core Image并没有内置类似于老电影的效果,就是那种影像有点发黄,同时还会带点黑条、白条之类的,而我们如果要实现这种效果,总体上就像这样:
大致过程如下:
- 需要使用CISepiaTone滤镜,CISepiaTone能使整体颜色偏棕褐色,又有点像复古
- 需要创建随机噪点图,很像以前电视机没信号时显示的图像,再通过它生成一张白斑图滤镜
- 需要创建另一个随机噪点图,然后通过它生成一张黑色磨砂图滤镜,就像是一张使用过的黑色砂纸一样
- 把它们组合起来
在开始之前首先要知道一件事,我们已经知道了一些简单的滤镜,它们只需要设置inputImage就行了;还有一些除了inputImage参数外有其他参数的滤镜,除此之外,还有一些滤镜不需要任何参数,就是上面提到的随机噪点图,另外,有些Core Image滤镜会生成无限大小的图,比如CICategoryTileEffect类别下的滤镜,在渲染它们生成的图之前,必须先把这些无限大小的图裁剪一番,你可以通过CICrop滤镜来完成这一步,也可以通过在一个有限的矩形范围之类渲染这张图来达到同样的效果。
然后我们就动手吧。
在VC里添加一个IBAction方法:oldFilmEffect,然后在Storyboard的VC上增加一个按钮,就叫“老电影”,然后连接到oldFilmEffect方法上,oldFilmEffect方法实现的代码稍后给出,这里先描述下详细步骤,其实通过这些详细步骤,已经可以自己先实现出来了:
应用CISepiaTone滤镜到原图上
- 设置inputImage为原图
- 设置inputIntensity为1.0
创建白斑图滤镜
用CIRandomGenerator生成随机噪点滤镜,然后通过imageByCroppingToRect方法对其进行裁剪,在imageByCroppingToRect方法内Core Image隐式的使用了CICrop滤镜。
接下来使用CIColorMatrix滤镜,该滤镜可以很方便的调整图片中RGBA各分量的值,其参数设置如下:
- 设置inputImage为CIRandomGenerator生成的随机噪点图
- 设置inputRVector、inputGVector和inputBVector为(0,1,0,0)
- 设置inputBiasVector为(0,0,0,0)
用CISourceOverCompositing(源覆盖)滤镜把前景图(inputImage)覆盖在背景图(inputBackgroundImage)上:
- 设置inputImage为CISepiaTone滤镜生成的图
- 设置inputBackgroundImage为白斑图滤镜
创建黑色磨砂图滤镜
还是先用CIRandomGenerator生成随机噪点图,然后用CIAffineTransform滤镜对其进行处理,其实就是把生成的点放大。参数设置如下:
- 设置inputImage为CIRandomGenerator生成的随机噪点图
- 设置inputTransform为x放大1.5倍、y放大25倍,把点拉长、拉厚,但是它们仍然是有颜色的
在这里除了使用CIAffineTransform滤镜外,还有一种替代方法可以达到同样的效果,同时不用显式创建CIAffineTransform滤镜,就是使用CIImage的imageByApplyingTransform:方法。
再次用CIColorMatrix滤镜对颜色进行处理:
- 设置inputImage为CIAffineTransform生成的图
- 设置inputRVector为(4,0,0,0)
- 设置inputGVector、inputBVector和inputAVector为(0,0,0,0)
- 设置inputBiasVector为(0,1,1,1)
现在产生的是一个蓝绿色磨砂图滤镜,再把CIMinimumComponent滤镜应用到这个蓝绿色磨砂图滤镜产生的图上。CIMinimumComponent滤镜会使用r、g、b的最小值生成一张灰度图像。
把所有的滤镜组合起来
使用CIMultiplyCompositing做最后的组合,参数设置如下:
- 设置inputImage为CISourceOverCompositing滤镜生成的图(内含CISepiaTone、白斑图滤镜的效果)
- 设置inputBackgroundImage为CIMinimumComponent滤镜生成的图(内含黑色磨砂图滤镜效果)
最后把CIMultiplyCompositing生成出的图输出到imageView上,还是以前的方式,先转成CGImage,再把CGImage转成UIImage。
有点小长,而且同时用到了多个滤镜,其实想表达的意思并没有那么复杂,可以使用kCICategoryBuiltIn把所有的滤镜打印出来,然后对照着看它们的参数。
这里是oldFilmEffect方法实现:
@IBAction func oldFilmEffect() {
let inputImage = CIImage(image: originalImage)
// 1.创建CISepiaTone滤镜
let sepiaToneFilter = CIFilter(name: "CISepiaTone")
sepiaToneFilter.setValue(inputImage, forKey: kCIInputImageKey)
sepiaToneFilter.setValue(1, forKey: kCIInputIntensityKey)
// 2.创建白斑图滤镜
let whiteSpecksFilter = CIFilter(name: "CIColorMatrix")
whiteSpecksFilter.setValue(CIFilter(name: "CIRandomGenerator").outputImage.imageByCroppingToRect(inputImage.extent()), forKey: kCIInputImageKey)
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputRVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputGVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputBVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBiasVector")
// 3.把CISepiaTone滤镜和白斑图滤镜以源覆盖(source over)的方式先组合起来
let sourceOverCompositingFilter = CIFilter(name: "CISourceOverCompositing")
sourceOverCompositingFilter.setValue(whiteSpecksFilter.outputImage, forKey: kCIInputBackgroundImageKey)
sourceOverCompositingFilter.setValue(sepiaToneFilter.outputImage, forKey: kCIInputImageKey)
// ---------上面算是完成了一半
// 4.用CIAffineTransform滤镜先对随机噪点图进行处理
let affineTransformFilter = CIFilter(name: "CIAffineTransform")
affineTransformFilter.setValue(CIFilter(name: "CIRandomGenerator").outputImage.imageByCroppingToRect(inputImage.extent()), forKey: kCIInputImageKey
affineTransformFilter.setValue(NSValue(CGAffineTransform: CGAffineTransformMakeScale(1.5, 25)), forKey: kCIInputTransformKey)
// 5.创建蓝绿色磨砂图滤镜
let darkScratchesFilter = CIFilter(name: "CIColorMatrix")
darkScratchesFilter.setValue(affineTransformFilter.outputImage, forKey: kCIInputImageKey)
darkScratchesFilter.setValue(CIVector(x: 4, y: 0, z: 0, w: 0), forKey: "inputRVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputAVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 1, z: 1, w: 1), forKey: "inputBiasVector")
// 6.用CIMinimumComponent滤镜把蓝绿色磨砂图滤镜处理成黑色磨砂图滤镜
let minimumComponentFilter = CIFilter(name: "CIMinimumComponent")
minimumComponentFilter.setValue(darkScratchesFilter.outputImage, forKey: kCIInputImageKey)
// ---------上面算是基本完成了
// 7.最终组合在一起
let multiplyCompositingFilter = CIFilter(name: "CIMultiplyCompositing")
multiplyCompositingFilter.setValue(minimumComponentFilter.outputImage, forKey: kCIInputBackgroundImageKey)
multiplyCompositingFilter.setValue(sourceOverCompositingFilter.outputImage, forKey: kCIInputImageKey)
// 8.最后输出
let outputImage = multiplyCompositingFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
以上就是一个老电影滤镜的“配方”了。
编译、运行,显示效果如下:
子类化CIFilter
有时可能会对一些图片应用同样的滤镜,我们可能会像上面那样把一连串的滤镜组合起来,以达到自己想要的效果,那么我们就可以把这些操作封装到一个CIFilter的子类中,然后在多个地方反复使用,就像使用Core Image预置的滤镜那样。
CICategoryColorEffect类别中有个CIColorInvert滤镜,这个滤镜提供反色功能,实现起来并不复杂,因为我们并不是做一个真正的自定义滤镜,而是在里面对Core Image已有滤镜的封装,我们可以为子类定义一些输入参数,参照苹果对CIFilter子类的命名约定,输入参数必须用input作前缀,如inputImage,然后再重写outputImage方法就行了。
现在我们回到Xcode中,做以下几件事:
- 新建一个Cocoa Touch Class,类名就叫CIColorInvert,继承自CIFilter。
- 添加一个inputImage参数,类型自然是CIImage,由外界赋值。
- 重写outputImage属性的getter。如果你之前写过Objective-C,应该对属性有这样一个印象:子类要重写父类的属性,只需要单独写个getter或setter方法就行了,但在Swift里,不能通过这种方式重写属性,必须连getter、setter(如果父类的属性支持setter的话)一起重写。在我们的例子中outputImage在CIFilter中只是一个getter属性,
- 在outputImage里通过CIColorMatrix滤镜对图像的各向量进行调整。
CIColorInvert类实现:
class CIColorInvert: CIFilter {
var inputImage: CIImage!
override var outputImage: CIImage! {
get {
return CIFilter(name: "CIColorMatrix", withInputParameters: [
kCIInputImageKey : inputImage,
"inputRVector" : CIVector(x: -1, y: 0, z: 0),
"inputGVector" : CIVector(x: 0, y: -1, z: 0),
"inputBVector" : CIVector(x: 0, y: 0, z: -1),
"inputBiasVector" : CIVector(x: 1, y: 1, z: 1),
]).outputImage
}
}
}
然后在Storyboard的VC上增加一个按钮“反色”,连接到VC的colorInvert方法上,colorInvert方法实现如下:
@IBAction func colorInvert() {
let colorInvertFilter = CIColorInvert()
colorInvertFilter.inputImage = CIImage(image: imageView.image)
let outputImage = colorInvertFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
这样一下,一个对Core Image预置滤镜的简单封装就完成了,每一个滤镜的效果就像是一张配方,CIFilter就是装有配方的瓶子,所以子类化CIFilter并不算自定义滤镜,但是从iOS 8开始,Core Image是支持真正的自定义滤镜的,自定义的滤镜被称之为内核(CIKernel),在WWDC视频里对其有50分钟的介绍:https://developer.apple.com/videos/wwdc/2014/#515。
运行后反色的效果,再次点击反色按钮后显示原图:
简单抠图并更换背景
利用Core Image预置的滤镜能满足大部分使用场景,我们做一个简单的替换背景的功能。
为了方便测试,加入两张新的图:
点击图片可以打开原图。
将两张图添加到当前工程中,然后把ViewController的属性originalImage改为返回左边的图:
......
lazy var originalImage: UIImage = {
return UIImage(named: "Image2")
}()
......
然后在Storyboard的VC上增加两个按钮:一个用于显示原图:
@IBAction func showOriginalImage() {
self.imageView.image = originalImage
}
另一个按钮就叫“更换背景”,连接到VC的IBAction方法replaceBackground上。
我们先看要做的事情:
- 消除深绿色
- 组合图片
消除深绿色
就像Photoshop的魔法棒一样,Core Image也有类似的滤镜,但是没有那么简单粗暴,使用起来很麻烦。
在Core Image里,我们为了消除某种颜色,需要使用CIColorCube滤镜,而CIColorCube滤镜需要一张cube映射表,这张表其实就是张颜色表(3D颜色查找表),把你想消除的颜色的alpha值设置为0,其他的颜色不变,Core Image将会把图像数据上的颜色映射为表中的颜色,以此来达到消除某种颜色的目的。
CIColorCube的这张表默认不会对inputImage作任何处理,但在我们这里要将所有的深绿色干掉,所以需要自己来建立这张表。
我们要消除的“深绿色”并不只是视觉上的一种颜色,而是颜色的范围,最直接的方法是将RGBA转成HSV(Hue,Saturation,Value),在HSV的格式下,颜色是围绕圆柱体中轴的角度来表现的,在这种表现方法下,你能把颜色的范围想象成连在一起的扇形,然后直接把该块区域干掉(alpha设为0),这就表示我们实际上需要指定颜色区域的范围------围绕圆柱体中轴线的最小角度以及最大角度,此范围内的颜色alpha设为0。最后,Cube Map表中的数据必须乘以alpha,所以创建Cube Map的最后一步是把RGB值乘以你刚刚计算出来的alpha值:如果是想要消除的颜色,乘出来就是0,反之则是1。这是一张代表颜色值区域的HSV(Hue值)图:
可以看到如果是纯绿色,其取值是120度,蓝色是240度,我们这种情况取值大概在60到90左右(偏绿一点),在这个网站上可以看到更详细的RGB颜色对应的HSV值。
那么接下来我们就准备创建Cube Map表,创建Cube Map表的方法在苹果官方示例中可以找到,是C语言实现的,为了方便起见,我们就直接创建一个C文件来包含这些代码。
在工程里选择新建一个.c文件,我取名为CubeMap.c,在创建这个.c文件的时候,不出意外的话Xcode会问你是否需要创建一个桥接头文件(xxxx.Bridging-Header.H),选择是,Xcode会创建该文件,并自动把其路径放到编译选项的Objective-C Bridging Header中。如果你要自己添加这个文件,并且需要手动修改Objective-C Bridging Header的编译选项,可以看这里。
.c文件搞完以后,即把苹果官方示例中的代码(以下代码)添加进去:
struct CubeMap {
int length;
float dimension;
float *data;
};
struct CubeMap createCubeMap(float minHueAngle, float maxHueAngle) {
const unsigned int size = 64;
struct CubeMap map;
map.length = size * size * size * sizeof (float) * 4;
map.dimension = size;
float *cubeData = (float *)malloc (map.length);
float rgb[3], hsv[3], *c = cubeData;
for (int z = 0; z < size; z++){
rgb[2] = ((double)z)/(size-1); // Blue value
for (int y = 0; y < size; y++){
rgb[1] = ((double)y)/(size-1); // Green value
for (int x = 0; x < size; x ++){
rgb[0] = ((double)x)/(size-1); // Red value
rgbToHSV(rgb,hsv);
// Use the hue value to determine which to make transparent
// The minimum and maximum hue angle depends on
// the color you want to remove
float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f: 1.0f;
// Calculate premultiplied alpha values for the cube
c[0] = rgb[0] * alpha;
c[1] = rgb[1] * alpha;
c[2] = rgb[2] * alpha;
c[3] = alpha;
c += 4; // advance our pointer into memory for the next color value
}
}
}
map.data = cubeData;
return map;
}
我将这个方法稍微改造了一下,选择一个结构体,因为外面要用到length和dimension。苹果没有提供rgbToHSV方法的实现,可以用我找到的这个:
void rgbToHSV(float *rgb, float *hsv) {
float min, max, delta;
float r = rgb[0], g = rgb[1], b = rgb[2];
float *h = hsv, *s = hsv + 1, *v = hsv + 2;
min = fmin(fmin(r, g), b );
max = fmax(fmax(r, g), b );
*v = max;
delta = max - min;
if( max != 0 )
*s = delta / max;
else {
*s = 0;
*h = -1;
return;
}
if( r == max )
*h = ( g - b ) / delta;
else if( g == max )
*h = 2 + ( b - r ) / delta;
else
*h = 4 + ( r - g ) / delta;
*h *= 60;
if( *h < 0 )
*h += 360;
}
我在.c文件中导入的库:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
对了,如果那个桥接文件里没有导入这个.c文件的话是不行的,Swift的类会找不到这里面的方法。
// ComplexFilters-Bridging-Header.h
// Use this file to import your target‘s public headers that you would like to expose to Swift.
//
#import "CubeMap.c"
组合图片
VC中的replaceBackground方法只需要做三件事:
- 创建Cube Map表
- 创建CIColorCube滤镜并使用Cube Map
- 用CISourceOverCompositing滤镜将处理过的人物图像和未处理过的背景图粘合起来
方法实现如下:
@IBAction func replaceBackground() {
let cubeMap = createCubeMap(60,90)
let data = NSData(bytesNoCopy: cubeMap.data, length: Int(cubeMap.length), freeWhenDone: true)
let colorCubeFilter = CIFilter(name: "CIColorCube")
colorCubeFilter.setValue(cubeMap.dimension, forKey: "inputCubeDimension")
colorCubeFilter.setValue(data, forKey: "inputCubeData")
colorCubeFilter.setValue(CIImage(image: imageView.image), forKey: kCIInputImageKey)
var outputImage = colorCubeFilter.outputImage
let sourceOverCompositingFilter = CIFilter(name: "CISourceOverCompositing")
sourceOverCompositingFilter.setValue(outputImage, forKey: kCIInputImageKey)
sourceOverCompositingFilter.setValue(CIImage(image: UIImage(named: "background")), forKey: kCIInputBackgroundImageKey)
outputImage = sourceOverCompositingFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
参数设置都还比较简单,CISourceOverCompositing滤镜目前已经使用过多次了,并没有什么复杂的。
编译、运行,可以分两次执行,先看消除深绿色的效果,再看最后使用CISourceOverCompositing滤镜组合图片之后的效果:
GitHub下载地址
我在GitHub上会保持更新。
参考资料:
http://www.docin.com/p-387777241.html