概述
滤镜最早的出现应该是应用在相机镜头前实现自然光过滤和调色的镜片,然而在软件开发中更多的指的是软件滤镜,是对镜头滤镜的模拟实现。当然这种方式更加方便快捷,缺点自然就是无法还原拍摄时的真实场景,例如无法实现偏光镜和紫外线滤色镜的效果。今天简单介绍一下iOS滤镜开发中的正确姿势,让刚刚接触滤镜开发的朋友少走弯路。
在iOS开发中常见的滤镜开发方式大概包括:CIFilter、GPUImage、OpenCV等。
CoreImage
CIFilter
CIFilter存在于CoreImage框架中,它基于OpenGL着色器来处理图像(最新的已经基于Metal实现),优点当然是快,因为它可以充分利用GPU加速来处理图像渲染,同时它自身支持滤镜链,多个滤镜同时使用时迅速高效。
CIFilter目前已经支持21个分类(如下代码)196种滤镜:
public let kCICategoryDistortionEffect: String
public let kCICategoryGeometryAdjustment: String
public let kCICategoryCompositeOperation: String
public let kCICategoryHalftoneEffect: String
public let kCICategoryColorAdjustment: String
public let kCICategoryColorEffect: String
public let kCICategoryTransition: String
public let kCICategoryTileEffect: String
public let kCICategoryGenerator: String
@available(iOS 5.0, *)
public let kCICategoryReduction: String
public let kCICategoryGradient: String
public let kCICategoryStylize: String
public let kCICategorySharpen: String
public let kCICategoryBlur: String
public let kCICategoryVideo: String
public let kCICategoryStillImage: String
public let kCICategoryInterlaced: String
public let kCICategoryNonSquarePixels: String
public let kCICategoryHighDynamicRange: String
public let kCICategoryBuiltIn: String
@available(iOS 9.0, *)
public let kCICategoryFilterGenerator: String
使用 open class func filterNames(inCategory category: String?) -> [String]
可以查看每个分类的滤镜名称。而每个滤镜的属性设置通过CIFilter的attributes就可以查看。而应用一个CIFilter滤镜也仅仅需要:创建滤镜->设置属性(KVC)->读取输入图片(下面演示了高斯模糊滤镜的简单实现):
guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
let ciImage = CIImage(cgImage: cgImage)
let filter = CIFilter(name: "CIGaussianBlur")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
filter?.setValue(5.0, forKey: "inputRadius")
if let outputImage = filter?.value(forKeyPath: kCIOutputImageKey) as? CIImage {
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
let image = UIImage(cgImage: cgImage)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
原图
应用高斯模糊
滤镜链
所谓滤镜链就是将一个滤镜A的输出作为另一个滤镜B的输入形成有向图,使用这种方式Core Image并非一步步执行结果应用到B滤镜,而是将多个滤镜的着色器合并操作,从而提高性能。
例如在上面的高斯模糊滤镜基础上应用像素化滤镜:guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return } let ciImage = CIImage(cgImage: cgImage) let blurFilter = CIFilter(name: "CIGaussianBlur") blurFilter?.setValue(ciImage, forKey: kCIInputImageKey) blurFilter?.setValue(5.0, forKey: "inputRadius") let pixelFilter = CIFilter(name: "CIPixellate", parameters: [kCIInputImageKey:blurFilter!.outputImage!]) pixelFilter?.setDefaults() if let outputImage = pixelFilter?.outputImage { let context = CIContext() if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { let image = UIImage(cgImage: cgImage) UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) } }
另外新的API(iOS 11)如果使用滤镜建议使用更加直观的表达以简化书写:
let outputImage = ciImage.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey:5.0]).applyingFilter("CIPixellate")
此外说到CoreImage的高斯模糊时直接使用是有一个问题的,那就是radius越大越会产生一个明显的空白边缘,当然这个问题是因为滤镜的卷积操作通常从中心点开始应用造成的,这样就会致使边缘上的像素值不能得到有效应用,类似于OpenCV会自己处理这个问题,但是Core Image并没有处理这个边缘问题,通常的处理方法就是放大图片,然后剪切到原来的图片大小即可(其实就是在滤镜前后分别调用clampedToExtend()获取一个边缘扩展的图像,应用滤镜之后调用croped()获取一个裁剪边缘的图像即可)。guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return } let ciImage = CIImage(cgImage: cgImage) let outputImage = ciImage.clampedToExtent().applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey:5.0]).cropped(to: ciImage.extent) let context = CIContext() if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) { let image = UIImage(cgImage: cgImage) UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) }
自定义算子
尽管Core Image提供了不少滤镜可以使用,不过实际开发中还并不能够满足需求,比如说描绘边缘这个操作在Core Image中应该就没有提供直接的滤镜。而有不少滤镜是通过卷积操作完成的,只要提供一个算子就可以形成一个新的滤镜效果,事实上Core Image框架也提供了这个滤镜:CIConvolution3X3和CIConvolution5X5。这两个滤镜支持开发者自定义算子实现一个滤镜操作,下面是使用CIConvolution3X3实现的sobel算子提取边缘的滤镜:
guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
let ciImage = CIImage(cgImage: cgImage)
let sobel:[CGFloat] = [-1,0,1,-2,0,2,-1,0,1]
let weight = CIVector(values: sobel, count: 9)
let outputImage = ciImage.applyingFilter("CIConvolution3X3", parameters: [kCIInputWeightsKey:weight,kCIInputBiasKey:0.5])
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) {
let image = UIImage(cgImage: cgImage)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
前面的图应用Sobel算子后的效果:
可以看出来边缘已经被提取出来,其实无论是CIConvolution3X3还是CIConvolution5X5都只是进行一个卷积操作,本质就是对应的像素分别乘以对应算子上的值最后相加等于产生一个新的值作为当前像素的值(这个值通常是待处理图像区块中心)如下图:
除了上面的Sobel算子,常见的算子还有锐化算子{0,-1,0,-1,5,-1,0,-1,0}、浮雕算子{1,0,0,0,0,0,0,0,-1}、拉普拉斯算子(边缘检测){0,1,0,1,-4,1,0,1,0}等等。
自定义滤镜
如果仅仅是自定义算子恐怕还不能体现出CIFilter的强大之处,毕竟不少滤镜通过特定算子还是无法满足的,CIFilter支持自定义片段着色器实现自己的滤镜效果。
自定义的 Filter 和系统内置的各种 CIFilter,使用起来方式是一样的。我们唯一要做的,就是实现一个符合规范的 CIFilter 的子类。过程大家就是:编写 kernel->加载 kernel->设置参数。假设现在编写一个图片翻转的效果大概过程如下:
1.编写kernel脚本,保存为Flip.kernel
kernel vec2 mirrorX ( float imageWidth )
{
vec2 currentVec = destCoord();
return vec2 ( imageWidth - currentVec.x , currentVec.y );
}
2.加载kernel
class FlipFilterGenerator:NSObject, CIFilterConstructor {
func filter(withName name: String) -> CIFilter? {
if name == "(FlipFilter.self)" {
return FlipFilter()
}
return nil
}
}
private let flipKernel:CIWarpKernel? = CIWarpKernel(source:try! String(contentsOf:Bundle.main.url(forResource: "Flip", withExtension: "cikernel")!))
class FlipFilter: CIFilter {
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func register() {
CIFilter.registerName("(FlipFilter.self)", constructor: FlipFilterGenerator(), classAttributes: [kCIAttributeFilterName:"(FlipFilter.self)"])
}
override func setDefaults() {
}
@objc var inputImage: CIImage?
override var outputImage: CIImage? {
guard let width = self.inputImage?.extent.size.width else { return nil }
let result = flipKernel?.apply(extent: inputImage!.extent, roiCallback: { (index, rect) -> CGRect in
return rect
}, image: self.inputImage!, arguments: [width])
return result
}
override var name: String {
get {
return "(FlipFilter.self)"
}
set {
}
}
}
使用CIFilter的source构造函数传入着色器代码,然后通过apply()方法传入参数即可执行着色。当然使用之前记得进行注册,这样在使用的时候就可以像使用内置滤镜一样使用了。
但是这里必须着重看一下apply()方法的几个参数
extent:要处理的输入图片的区域(称之为DOD ( domain of definition ) ),一般处理的都是原图,并不会改变图像尺寸所以上面传的是inputImage.extent
roiCallback:感兴趣的处理区域(ROI ( region of interest ),可以理解为当前处理区域对应的原图区域)处理完后的回调,回调参数index代表图片索引顺序,回调参数rect代表输出图片的区域DOD,但是需要注意在Core Image处理中这个回调会多次调用。这个值通常只要不发生旋转就是当前图片的坐标(如果旋转90°,则返回为CGRect(x: rect.origin.y, y: rect.origin.x, rect.size.height, height: rect.size.width))
arguments:着色器函数中需要的参数,按顺序传入。
自定义滤镜调用:
FlipFilter.register()
guard let cgImage = UIImage(named:"CIFilter_Demo_Origin")?.cgImage else { return }
let ciImage = CIImage(cgImage: cgImage)
let outputImage = ciImage.applyingFilter("FlipFilter")
let context = CIContext()
if let cgImage = context.createCGImage(outputImage, from: ciImage.extent) {
let image = UIImage(cgImage: cgImage)
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
下面是上图使用翻转滤镜后的效果:
其实准确的来说实现一个自定义滤镜就是实现一个自定义的CIKernel类,当然这个类本身包括两个子类CIColorKernel和CIWarpKernel,前者用于图像颜色转化滤镜,而后者用于形变滤镜,如前面的翻转很明显不是一个颜色值的修改就能解决的,必须依赖于形变操作所以继承自CIWarpKernel要简单些。当然如果你的滤镜综合了二者的特点那么直接选择使用CIKernel是正确的。至于着色器代码编写使用的是Core Image Kernel Language (CIKL),它是OpenGL Shading Language (GLSL) 的子集。CIKL 集成了 GLSL 绝大部分的参数类型和内置函数,另外它还添加了一些适应 Core Image 的参数类似和函数。另外编写CIKL需要注意坐标系,它的坐标系从左下角开始而不是UIKit的左上角。
由于篇幅原因关于编写CIKL的具体细节这里不再赘述,感兴趣可以参考Writing Kernels和Core Image Kernel Language Reference,而编写CIKL的工具自然推荐官方的Quartz Composer。
从前面的演示也可以看到图片在UIImage、CGImage和CIImage之间不停的转化,那么三者之间有什么区别呢?
UIImage存在于UIKit中,CGImage存在于Core Graphics中,CIImage存在于Core Image中。前者负责展示和管理图片数据,例如可以使用UIImageView展示、或者绘制到UIView、layer上等,主要在CPU上操作;CGImage表示图像的像素矩阵,每个点都对应了图片的像素信息,主要运行在GPU上;而CIImage包含了创建图片的必要数据,自身并不会渲染成图片,代表了图像的数据或者操作图像的流程(如滤镜),主要运行在GPU上。换句话说对于CIImage的操作并不会进行大量的图片运算,只有要输出图片时才需要转化成图片数据(推荐这一步尽量放到异步线程中操作)。
注意:获取一个图片的CIImage类型时请使用CIImage()构造方法创建,请勿直接访问uiImage.ciImage,因为如果一个UIImage不是从CIImage创建是无法获取ciImage的(uiImage.cgImage类似,上面之所以可以直接使用UIImage.cgImage属性是因为它并非从ciImage创建)。反之,如果从ciImage创建UIImage就不推荐使用UIImage的构造方法了,因为这种方式会丢失信息,例如使用UIViewImage显示时会丢失contentMode设置,如果使用上面的代码保存会出现保存失败的情况,推荐的方式则是使用UIContext先生成CGImage,然后从CGImage创建UIImage(总结起来就是UIImage到CGImage明确的情况下可以直接访问cgImage属性,但是cgImage为空则访问ciImage属性再从ciImage创建cgImage,从CGImage转化为UIImage使用构造函数;UIImage到CIImage推荐使用构造函数,也可以使用CGImage从中间过渡,而从CIImage转化为UIImage只能通过CGImage过渡再用构造函数创建)。
Metal Shader
如果你编写过CIKL你会发现这种开发方式很古老,Quartz Composer尽管作为目前开发CIKL最合适的工具但在Xcode7之后几乎没有更新过,尽管有语法高亮但是没有错误调试,更不用说运行时出错的问题(尽管可以使用+(id)kernelsWithString:(id)arg1 messageLog:(id)arg2这个私有方法打印kernel中的错误,但是调试依然很麻烦),自身以字符串传入CIKernel类的方式让它天然失去了语法检查。更重要的是这种方式最终要将CIKL片段变成CIKernel必须经过CIKL->GLSL->CIKernel->IL->GPU识别码->Render到GPU,如果遇到滤镜链还必须在中间链接Kernel,而这些操作全部在运行时进行。所以首次使用会比较慢(后面使用会缓存),而2017年Metal支持CIKernel则将Kernel的编译提前到了App编译阶段,从而支持了语法检查,大大提高了开发效率和运行效率。
例如前面的滤镜链中使用了一个马赛克风格的滤镜,这里不妨先看一下使用CIKL编写这个滤镜(注意这是一个CIWrapKernel,返回值是变化后的坐标位置):
kernel vec2 pixellateKernel(float radius)
{
vec2 positionOfDestPixel, centerPoint;
positionOfDestPixel = destCoord();
centerPoint.x = positionOfDestPixel.x - mod(positionOfDestPixel.x, radius * 2.0) + radius;
centerPoint.y = positionOfDestPixel.y - mod(positionOfDestPixel.y, radius * 2.0) + radius;
return centerPoint;
}
这个CIKL用Metal Shader书写如下:
extern "C" {
namespace coreimage {
float2 pixellateMetal(float radius,destination dest) {
float2 positionOfDestPixel, centerPoint;
positionOfDestPixel = dest.coord();
centerPoint.x = positionOfDestPixel.x - fmod(positionOfDestPixel.x, radius * 2.0) + radius;
centerPoint.y = positionOfDestPixel.y - fmod(positionOfDestPixel.y, radius * 2.0) + radius;
return centerPoint;
}
}
}
当然对应的自定义CIFilter需要做少许调整:
class PixellateFilterGenerator:NSObject, CIFilterConstructor {
func filter(withName name: String) -> CIFilter? {
if name == "(PixellateFilter.self)" {
return PixellateFilter()
}
return nil
}
}
private var pixellateKernel:CIWarpKernel? = {
guard let url = Bundle.main.url(forResource: "default", withExtension: "metallib") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
let kernel = try? CIWarpKernel(functionName: "pixellateMetal", fromMetalLibraryData: data)
return kernel
}()
class PixellateFilter: CIFilter {
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func register() {
CIFilter.registerName("(PixellateFilter.self)", constructor: PixellateFilterGenerator(), classAttributes: [kCIAttributeFilterName:"(PixellateFilter.self)"])
}
override func setDefaults() {
}
@objc var inputImage: CIImage?
@objc var radius:CGFloat = 5.0
override var outputImage: CIImage? {
let result = pixellateKernel?.apply(extent: inputImage!.extent, roiCallback: { (index, rect) -> CGRect in
return rect
}, image: self.inputImage!, arguments: [radius])
return result
}
override var name: String {
get {
return "(PixellateFilter.self)"
}
set {
}
}
override var attributes: [String : Any] {
get {
return [
"radius":[
kCIAttributeMin:1,
kCIAttributeDefault:5.0,
kCIAttributeType:kCIAttributeTypeScalar
]
]
}
}
}
如果说只是像前面一样简单的使用这个滤镜恐怕还无法体现Metal Shader的高性能,不妨把上面应用自定义滤镜后直接保存相册的操作改成一个滑动条在UIImageView直接预览:
class ViewController: UIViewController {
var filter:CIFilter?
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.imageView)
self.view.addSubview(sliderBar)
PixellateFilter.register()
filter = CIFilter(name: "PixellateFilter")
guard let cgImage = UIImage(named: "CIFilter_Demo_Origin")?.cgImage else { return }
let ciImage = CIImage(cgImage: cgImage)
filter?.setValue(ciImage, forKey: kCIInputImageKey)
}
@objc func sliderValueChange(_ sender:UISlider) {
filter?.setValue(sender.value, forKey: "radius")
if let outputImage = filter?.outputImage {
self.imageView.image = UIImage(ciImage: outputImage)
}
}
private lazy var imageView:UIImageView = {
let temp = UIImageView(frame: CGRect(x: 0.0, y: 0.0, Constants.screenSize.width, height: Constants.screenSize.height-60))
temp.contentMode = .scaleAspectFill
temp.image = UIImage(named: "CIFilter_Demo_Origin")
return temp
}()
private lazy var sliderBar:UISlider = {
let temp = UISlider(frame: CGRect(x: 0.0, y: Constants.screenSize.height-50, Constants.screenSize.width, height: 30))
temp.minimumValue = 1
temp.maximumValue = 20
temp.addTarget(self, action: #selector(sliderValueChange(_:)), for: UIControl.Event.valueChanged)
return temp
}()
}
运行效果:
可以看到,拖动滑动条可以实时预览滤镜效果而没有丝毫卡顿,前面也提到CIImage本身并不包含图像数据,当UIImageView显示时会在GPU上执行Core Image操作,释放了CPU的压力(这也是UIImageView针对Core Image优化的结果)。
无论是通过CIKL还是通过Metal自定义CIFilter都不是万能的,这是由于kernel本身的限制所造成的。kernel的原理简单理解就是遍历一个图片的所有像素点,然后通过kernel处理后返回新的像素点作为新的图片的像素点。而类似于绘制直方图、动漫风格等操作依赖于整个图片的分布或者依赖于机器学习的操作则很难使用kernel完成,当然这可以借助于后面的OpenCV轻松做到。
GPUImage
GPUImage可以说是iOS滤镜开发中多数app的首选,原因在于它不仅高效(从名字就可以看出它运行在GPU上),而且简单(下面三行代码就实现了上面的高斯模糊效果),当然还有它强大的工具属性。它不仅支持实时滤镜预览,还支持视频实时滤镜等。
下面是使用高斯模糊的演示:
GPUImageGaussianBlurFilter * blurFilter = [[GPUImageGaussianBlurFilter alloc] init];
blurFilter.blurRadiusInPixels = 2.0;
UIImage * image = [UIImage imageNamed:@"CIFilter_Demo_Origin"];
UIImage *blurredImage = [blurFilter imageByFilteringImage:image];
滤镜后的效果:
不过可以对比之前的效果,发现GPUImage对于高斯模糊的处理包括了边缘的处理,并不需要针对边缘进行重新裁剪。
当然如果不支持自定义那么GPUImage也谈不上强大,GPUImage 自定义滤镜需要使用 OpenGL 着色语言( GLSL )编写 Fragment Shader(片段着色器),这些其实和自定义Core Image是类似的。
下面演示了使用GPUImage自定义实现一个图片暗角滤镜:
#import <GPUImage/GPUImage.h>
@interface VignetteFilter : GPUImageFilter
@property (nonatomic,assign) CGPoint center;
@property (nonatomic,assign) CGFloat radius;
@property (nonatomic,assign) CGFloat alpha;
@end
@implementation VignetteFilter {
GLint centerXUniform,centerYUniform,alphaUniform,radiusUniform;
}
- (instancetype)init
{
self = [super initWithFragmentShaderFromFile:@"VignetteFilter"];
if (!self) {
return nil;
}
centerXUniform = [filterProgram uniformIndex:@"centerX"];
centerYUniform = [filterProgram uniformIndex:@"centerY"];
alphaUniform = [filterProgram uniformIndex:@"alpha"];
radiusUniform = [filterProgram uniformIndex:@"radius"];
self.alpha = 0.5;
self.radius = 100;
return self;
}
- (void)setCenter:(CGPoint)center {
[self setFloat:center.x forUniform:centerXUniform program:filterProgram];
[self setFloat:center.y forUniform:centerYUniform program:filterProgram];
}
- (void)setAlpha:(CGFloat)alpha {
[self setFloat:alpha forUniform:alphaUniform program:filterProgram];
}
- (void)setRadius:(CGFloat)radius {
[self setFloat:radius forUniform:radiusUniform program:filterProgram];
}
@end
片段着色器代码:
uniform highp float alpha;
uniform lowp float radius;
uniform lowp float centerX;
uniform lowp float centerY;
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
highp vec2 centerPoint = vec2(centerX, centerY);
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
highp float distance = distance(gl_FragCoord.xy, centerPoint);
highp float darken = 1.0 - (distance / (radius*0.5) * alpha);
gl_FragColor = vec4(textureColor.rgb*darken,textureColor.a);
}
滤镜后的图片
和Core Image不同的是GPUImage使用的并非CIKL而是GLSL(二者均是类C语言)来编写滤镜,优点自然是了解片段着色器就可以无过渡编写滤镜着色代码,无需转化,同时它也是跨平台的。缺点就是iOS 12之后Core Image使用Metal引擎逐渐摒弃了OpenGL,效率则更高(当然GPUImage3已经支持Metal Shader,这样二者就逐渐没有了区别)。
OpenCV
既然前面提到了OpenGL,那么就离不开另外一个库OpenCV,前者主要用于显示,后者用于运算处理,当然OpenCV默认编译是不支持的GPU加速的,不过胜在它的算法强大,算法速度很快,而且令人兴奋的是3.0以后使用CUDA是可以支持使用GPU运算的。
使用OpenCV实现滤镜更像是使用vImage(存在于Accelerate.framework),不仅可以像上面一样直接基于像素进行处理,还能使用它提供的很多强大算法,同时考虑到自定义算子OpenCV甚至直接暴漏了Filter2D让我们可以直接像编写上面的着色器那样方便的进行卷积操作。
下面使用OpenCV实现一个羽化操作:
#include <math.h>
#include <opencv/cv.h>
#include <opencv/highgui.h>
#define MAXSIZE (32768)
using namespace cv;
using namespace std;
float mSize = 0.5;
int main()
{
Mat src = imread("/Users/Kenshin/Downloads/CIFilter_Demo_Origin.jpg",1);
imshow("src",src);
int width=src.cols;
int heigh=src.rows;
int centerX=width>>1;
int centerY=heigh>>1;
int maxV=centerX*centerX+centerY*centerY;
int minV=(int)(maxV*(1-mSize));
int diff= maxV -minV;
float ratio = width >heigh ? (float)heigh/(float)width : (float)width/(float)heigh;
Mat img;
src.copyTo(img);
Scalar avg=mean(src);
Mat dst(img.size(),CV_8UC3);
Mat mask1u[3];
float tmp,r;
for (int y=0;y<heigh;y++)
{
uchar* imgP=img.ptr<uchar>(y);
uchar* dstP=dst.ptr<uchar>(y);
for (int x=0;x<width;x++)
{
int b=imgP[3*x];
int g=imgP[3*x+1];
int r=imgP[3*x+2];
float dx=centerX-x;
float dy=centerY-y;
if(width > heigh)
dx= (dx*ratio);
else
dy = (dy*ratio);
int dstSq = dx*dx + dy*dy;
float v = ((float) dstSq / diff)*255;
r = (int)(r +v);
g = (int)(g +v);
b = (int)(b +v);
r = (r>255 ? 255 : (r<0? 0 : r));
g = (g>255 ? 255 : (g<0? 0 : g));
b = (b>255 ? 255 : (b<0? 0 : b));
dstP[3*x] = (uchar)b;
dstP[3*x+1] = (uchar)g;
dstP[3*x+2] = (uchar)r;
}
}
imshow("blur",dst);
waitKey();
imwrite("/Users/Kenshin/Downloads/blur.jpg",dst);
}
没错,这是一段c++代码,但在OC中可以很方便的使用,只要实现一个Wrapper类,将.m改为.mm就可以直接调用c++代码。
下面是羽化后的效果:
总结
从上面可以看到其实开发滤镜选择很多,普通的滤镜使用GPUImage这种基于OpenGL的滤镜效率比较高、可移植性强,缺点当然就是GLSL调试比较难,遇到错误需要反复试验。如果你的App仅仅考虑iOS 11以上的运行环境,自然首推Metal Shading Language,调试方便又高效,尽管GPUImage3已经支持了Metal Shader但是当前还不完善,很多GPUImage有的功能还在待开发阶段当前不建议使用。而OpenCV自然是一把倚天剑,强大的算法,天然的可移植性,但是由于过于强大,不是类似于人脸识别这种复杂的非着色滤镜不推荐使用,当然换句话说一旦遇到机器学习相关(例如CARTOONGAN),高级特效一般非OpenCV莫属。